From a4440d6ba2d140fc8b028334fd79cfa8c55ea2f5 Mon Sep 17 00:00:00 2001 From: Amit Bahree Date: Thu, 28 May 2026 10:52:59 -0700 Subject: [PATCH 1/2] Refactor code and documentation across chapters for improved clarity and compatibility - Updated `quickstart.py` to enhance dataset preparation logic and improve length filtering feedback. - Revised `README.md` in chapter 3 to clarify package requirements and installation instructions, removing references to `unsloth`. - Modified `ch03_data_quality_explore.py` and `ch03_data_quality_helpers.py` to replace `unsloth` with `transformers + PEFT + TRL` for model training. - Enhanced `rag_minimal.py` with stable token hashing to ensure consistent embeddings across runs. - Updated `README.md` in chapter 5 to reflect Python version requirement change from 3.10 to 3.12 and clarified QLoRA compatibility on Apple Silicon. - Adjusted dataset preparation script in chapter 5 to ensure consistent UTC timestamp handling. - Updated `pyproject.toml` to reflect new dependency versions and compatibility notes. - Added `ACCELERATORS.md` to provide detailed guidance on running the code across different hardware setups and clarify GPU requirements. --- .github/workflows/code-ci.yml | 2 +- ACCELERATORS.md | 129 ++ README.md | 62 +- code/README.md | 561 ++++--- code/chapter02/README.md | 44 +- code/chapter02/ch02-openfinance-finetuning.py | 1474 ----------------- code/chapter02/ch02_openfinance_finetuning.py | 1467 ---------------- code/chapter02/quickstart.py | 14 +- code/chapter03/README.md | 36 +- code/chapter03/ch03_data_quality_explore.py | 16 +- code/chapter03/ch03_data_quality_helpers.py | 182 +- code/chapter04/rag_minimal.py | 16 +- code/chapter05/README.md | 4 +- .../scripts/listing_5_2_prepare_dataset.py | 2 +- code/pyproject.toml | 28 +- 15 files changed, 645 insertions(+), 3392 deletions(-) create mode 100644 ACCELERATORS.md delete mode 100644 code/chapter02/ch02-openfinance-finetuning.py delete mode 100644 code/chapter02/ch02_openfinance_finetuning.py diff --git a/.github/workflows/code-ci.yml b/.github/workflows/code-ci.yml index 038f5df..2d1b9f3 100644 --- a/.github/workflows/code-ci.yml +++ b/.github/workflows/code-ci.yml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python: ["3.11"] + python: ["3.12"] steps: - uses: actions/checkout@v4 diff --git a/ACCELERATORS.md b/ACCELERATORS.md new file mode 100644 index 0000000..e7b0414 --- /dev/null +++ b/ACCELERATORS.md @@ -0,0 +1,129 @@ +# Accelerators and environment guide + +Everything about running this book's code on real hardware: which chapters run +on which accelerator, GPU memory requirements, the setups we validated, the +dependency versions, performance across GPUs, and the design insights behind it +all. The [README](README.md) covers what the book is and how to start; this file +is the hardware reference it links to. + +The short version: the full book runs on **NVIDIA (CUDA)** and **AMD (ROCm)**. +**Apple Silicon (MPS)** runs everything except 4-bit QLoRA and the +full-parameter training chapters. A CPU-only machine runs the lightweight +chapters but is impractical for training. + +## What runs where + +| Chapter | NVIDIA (CUDA) | Apple Silicon (MPS) | AMD (ROCm) | Notes | +|---|:---:|:---:|:---:|---| +| 1 - sidebar reproducer | ✓ | ✓ | ✓ | Base-only mode is inference; the LoRA/SFT branches need the chapter 5/6 artifacts | +| 2 - LoRA quickstart | ✓ | ✓ | ✓ | Small adapter; the lightest training in the book | +| 3 - data-quality experiment | ✓ | ✓ | ✓ | bf16 LoRA across four data conditions | +| 4 - ICL / RAG | ✓ | ✓ | ✓ | CPU-friendly; GPU optional | +| 5 - LoRA | ✓ | ✓ | ✓ | | +| 5 - QLoRA (4-bit) | ✓ | ✗ | ✓ | `bitsandbytes` 4-bit is CUDA/ROCm-only; on a Mac use the LoRA path | +| 6 - full-parameter SFT | ✓ | ✗ | ✓ | Needs ~24 GB; a 16 GB Mac runs out of memory (~18 GB) | +| 7 - distillation | ✓ | ✗ | ✓ | Hosts the chapter 6 teacher; same memory profile as chapter 6 | +| 8 - DPO | ✓ | ✗ | ✓ | Full-model preference optimisation; same memory profile | +| 9 - drift / registry / monitor | ✓ | ✓ | ✓ | Registry, drift detector, and rollback are CPU/stdlib; canary and safety monitor are inference | + +✓ = validated to run. ✗ = does not run on that accelerator (use NVIDIA, AMD, or a cloud GPU). On Apple Silicon, training is correct but slower than on a GPU, so give it at least 16 GB of unified memory. + +## Which accelerator do I need? + +- **Any NVIDIA GPU with enough VRAM** runs everything; this is the reference path. +- **An AMD GPU on Linux (ROCm)** also runs everything, including QLoRA and the full-parameter chapters. Best on datacenter (MI-series) cards; consumer RDNA support varies by GPU generation. +- **A Mac (Apple Silicon)** is great for chapters 1 through 5's LoRA path and chapter 9, but cannot do QLoRA or the full-parameter chapters (6, 7, 8). For those, use a cloud GPU. +- **No GPU?** The lightweight chapters (4, the chapter 9 CPU stages, mock backends) run on CPU; training chapters are impractical. + +## GPU requirements at a glance + +AMD (ROCm) was validated end-to-end on a datacenter card (Instinct MI300X); VRAM needs match the NVIDIA column, and consumer RDNA support varies by GPU generation. See the [AMD note](#amd-gpu-notes) below. + +| Chapter | Minimum NVIDIA GPU | Recommended | AMD (ROCm) | CPU fallback | Apple Silicon (MPS) | +|---|---|---|---|---|---| +| 1 (sidebar reproducer) | None for base-only mode; 8 GB+ for the LoRA / SFT branches | 12 GB+ | Yes | Yes (base-only, slow) | Yes (base-only mode, slow on 8 GB unified memory) | +| 2 (LoRA quick-start) | 6 GB | 12 GB+ | Yes | Yes (slow) | Yes, verified on Apple M4/16 GB (quickstart trains on MPS in ~7 min) | +| 3 (data-quality experiment) | 8 GB | 12 GB+ | Yes | Synthetic-data pipeline yes; manifest module yes; full experiment slow | Yes, verified (bf16 LoRA on MPS); synthetic-data pipeline and manifest module also yes | +| 4 (ICL/RAG) | None for mock backends; ~8 GB for the optional Qwen3-4B HF backend | 12 GB+ | Yes | Yes (mock backend / hash embedder) | Yes (mock backends are CPU; HF backend uses MPS, slow on 8 GB) | +| 5 (LoRA) | 8 GB (RTX 3060/4060+) | 12 GB+ | Yes | Yes, but ~20× slower | Yes (trains on MPS) | +| 5 (QLoRA) | 6 GB | 8 GB+ | Yes (4-bit works; benign `rocminfo` warning) | Not recommended | **No** (`bitsandbytes` is CUDA-only) | +| 6 (Full SFT) | 24 GB (A30 / RTX 4090) | A100 40 GB+ | Yes | No | No | +| 7 (Distillation) | 12 GB (LoRA student) + 24 GB to host the chapter 6 teacher | 24 GB+ | Yes | Not recommended | No | +| 8 (DPO) | 24 GB | A100 40 GB+ | Yes | No | No | +| 9 (Drift / Registry / Monitor) | None for the CPU stages (registry, drift detector, rollback demo); ~8 GB for the GPU stages (canary, safety monitor) | 12 GB+ | Yes | Yes for stages 1, 2, and 4 | Yes for stages 1, 2, and 4 | + +**Disk space:** budget about 50 GB free for the Hugging Face model cache plus chapter 6's run directory (full-parameter checkpoints with optimizer state are 22-24 GB each). See `code/chapter06/README.md` for the breakdown. + +## Why QLoRA needs an NVIDIA or AMD GPU + +QLoRA is LoRA applied on top of a 4-bit quantized base model, and that 4-bit quantization is provided by `bitsandbytes`, whose kernels exist only for CUDA and ROCm (there is no Apple Metal/MPS build). On a Mac, use chapter 5's LoRA path, which needs no `bitsandbytes` and trains on MPS. QLoRA is a technique for fitting training into limited GPU memory, not a Mac feature, so this is expected rather than a gap. `bitsandbytes` is an optional extra in the package for exactly this reason: install it only when you want the QLoRA path. + +## Apple Silicon notes + +We verified the following on an Apple M4 (16 GB), macOS 15.6.1, using PyTorch's MPS (Metal) backend. These run locally: chapter 1 in base-only mode, the chapter 2 LoRA quickstart, chapter 3's data-quality experiment, chapter 4, chapter 5's LoRA training, and chapter 9's CPU stages. Training on MPS is slower than on a GPU, so give it a Mac with at least 16 GB of unified memory. Two things will not run on Apple Silicon: chapter 5's QLoRA (the `bitsandbytes` 4-bit kernels are CUDA/ROCm-only, see above), and the full-parameter training in chapters 6, 7, and 8 (chapter 6 ran out of memory at roughly 18 GB on the 16 GB Mac). Run those on Google Colab, a cloud GPU, or any CUDA/ROCm machine. + +## AMD GPU notes + +The PyTorch ROCm stack supports AMD GPUs on Linux, and we validated the full book on it. On an AMD Instinct MI300X (192 GB) running ROCm 7.x with the PyTorch ROCm wheel, every chapter ran end-to-end, including chapter 5 QLoRA (4-bit via `bitsandbytes`) and the full-parameter training in chapters 6, 7, and 8. So the chapters that a Mac cannot run do run on a ROCm machine. It works best on datacenter (MI-series) cards; consumer RDNA support varies by GPU generation. One caveat: `bitsandbytes` may print a warning that it could not find `rocminfo` and is defaulting the warp size to 64 (correct for CDNA cards like the MI300X); 4-bit training still works, and installing the ROCm command-line tools silences it. The fail-fast checks treat a working ROCm install as a GPU, so they let you proceed. + +## Validated environments + +The code is not pinned to one accelerator. The table records the exact machines and versions we verified on, so you know what is known-good. Other versions in the same range generally work; these are simply what we tested. + +| Accelerator | Machine | OS | Driver / runtime | PyTorch | Coverage | +|---|---|---|---|---|---| +| NVIDIA CUDA | A30 (24 GB) | Linux | CUDA 12.x | 2.11+cu126 | All chapters (reference platform for the book's published numbers) | +| NVIDIA CUDA | H200 (141 GB) | Ubuntu 22.04 | driver 590.48.01, compute 9.0 | 2.12.0+cu126 | Full book end-to-end | +| Apple Silicon (MPS) | Apple M4, 16 GB | macOS 15.6.1 | Metal / MPS | 2.12.0 | Ch1 base-only, Ch2 quickstart, Ch3 experiment, Ch4, Ch5 LoRA, Ch9 CPU stages. Not Ch5 QLoRA or full-parameter Ch6/7/8. | +| AMD ROCm | Instinct MI300X (192 GB) | Ubuntu 24.04 | ROCm 7.x, HIP 7.0.51831 | 2.10.0+rocm7.0 | Full book end-to-end, including Ch5 QLoRA (4-bit) and full-parameter Ch6/7/8 | + +## Dependency versions + +All accelerators run the same code on **Python 3.12** with `transformers` 4.57.6, `peft` 0.17.1, `trl` 1.5.x, `bitsandbytes` 0.49.2 (CUDA and ROCm only). The package pins `transformers>=4.47.0,<5.0`: transformers 5.x removes a symbol that `peft` imports, so a fresh install without the upper bound resolves to 5.x and breaks `import peft`. The per-accelerator PyTorch install command is in the [README Quick start](README.md#quick-start). + +## Performance across GPUs + +All three GPUs do a full end-to-end pass (every chapter, real 1-epoch training) with the same code and Python 3.12. Per-step wall time in seconds: + +| Step | A30 (24 GB) | MI300X (192 GB) | H200 (141 GB) | +|---|---:|---:|---:| +| Ch2 quickstart | 124 | 80 | 66 | +| Ch3 data-quality experiment | 750 | 435 | 385 | +| Ch5 train LoRA | 232 | 133 | 118 | +| Ch5 eval LoRA | 1008 | re-run* | 369 | +| Ch5 train QLoRA | 296 | 178 | 162 | +| Ch5 eval QLoRA | 924 | re-run* | 359 | +| Ch6 train SFT (full-parameter) | 217 | 122 | 118 | +| Ch7 generate teacher data | 620 | 360 | 328 | +| Ch7 train student | 62 | 44 | 31 | +| Ch7 eval distillation | 895 | 368 | 390 | +| Ch8 prepare preference data | 655 | 299 | 267 | +| Ch8 train DPO | 139 | 66 | 56 | +| Ch8 eval DPO | 763 | 381 | 331 | +| **Full pass (step time)** | **~112 min** | **~53 min** | **~50 min** | + +\* The MI300X run's Ch5 eval steps initially failed on a stale documented flag and were re-run separately, so they are not in that run's per-step summary. + +The A30 (Ampere, 2020) is roughly 2x the datacenter cards on training and ~2.5-3x on the generation-heavy eval steps. The H200 and MI300X are close to each other; a 4B model on small datasets does not stress either, so the gap between them would only open up with larger models, longer context, or bigger batches. + +## Results reproduce across accelerators + +Same code, same direction of results on every GPU (small numeric differences come from sampling and data subsetting): + +| Metric | A30 | MI300X | H200 | +|---|---|---|---| +| Ch3 A/B/C/D accuracy | 97 / 98 / 96 / 84% | 93 / 98 / 97 / 82% | 98 / 98 / 96 / 87% | +| Ch7 base / teacher / student F1 | 0.262 / 0.564 / 0.487 | 0.288 / 0.466 / 0.471 | 0.258 / 0.406 / 0.459 | +| Ch8 base / SFT / DPO F1 | 0.257 / 0.356 / 0.378 | 0.255 / 0.341 / 0.364 | 0.257 / 0.339 / 0.353 | + +The ordering is stable: chapter 3's corrupted condition is always the worst, the chapter 7 student approaches or matches its teacher (and always beats the base), and chapter 8 ranks DPO above SFT above base. + +## Insights + +From running every chapter on every accelerator: + +1. **Capability is a wall, not a slope.** The question that matters is "can my hardware run this at all," not "how fast." NVIDIA and AMD run the whole book; Apple Silicon's two gaps (no 4-bit `bitsandbytes` kernels, full-parameter OOM) are hard walls, not slowness. +2. **Generation dominates runtime, not training.** The longest steps are the eval and teacher-generation passes; a 1-epoch training step is much shorter. Speeding up these runs means faster decoding (batching, shorter outputs), not a bigger training GPU. +3. **QLoRA is a memory technique, not a speed one.** On a big card QLoRA was slower than plain LoRA: 4-bit quant/dequant is overhead with no memory pressure to relieve. Its value is fitting training into limited VRAM. +4. **Datacenter GPUs are interchangeable here.** H200 and MI300X finish in about the same time; the book's 4B/small-data workload does not stress either. +5. **The A30 stays the reference.** It is the realistic "what most readers have" floor and the platform behind the book's published numbers; the bigger cards confirm the code scales up cleanly. diff --git a/README.md b/README.md index 5fcdf60..d7184ac 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This repository contains all the runnable code, data, and examples from the book **Book Publisher:** Manning Publications (forthcoming) **Repository:** +> **Which hardware do I need?** The full book runs on NVIDIA (CUDA) and AMD (ROCm) GPUs. Most of it also runs on Apple Silicon (MPS), except 4-bit QLoRA and the full-parameter training chapters (6, 7, and 8). See **[ACCELERATORS.md](ACCELERATORS.md)** for the per-chapter breakdown, GPU memory requirements, the setups we validated, dependency versions, and performance across GPUs. + ## What is in this repo? | Folder | Contents | @@ -23,7 +25,7 @@ Every chapter ships with runnable code. The hands-on chapters (4 through 9) repr | Chapter and topic | What you build | |---|---| | **[Chapter 1: Why Model Adaptation?](code/chapter01/README.md)** | A reproducibility script for the §1.6 sidebar. Runs the same prompt through base Qwen3-4B, the Chapter 5 LoRA adapter, and the Chapter 6 SFT model side by side; degrades gracefully if the later-chapter artifacts are not yet built. | -| **[Chapter 2: How Do I Do Model Adaptation?](code/chapter02/README.md)** | Unsloth-based fine-tuning quick-start that reproduces the Dragon LLM open-finance recipe on Qwen3-0.6B in 15-25 minutes on a single consumer GPU. End-to-end pipeline: data preparation across four HF datasets, LoRA training via TRL's `SFTTrainer`, five evaluation tests, and model export (LoRA adapter, merged 16-bit, GGUF). | +| **[Chapter 2: How Do I Do Model Adaptation?](code/chapter02/README.md)** | A five-step LoRA fine-tuning quickstart on Qwen3-4B-Instruct-2507 using a 40-example Dolly subset (TRL's `SFTTrainer` plus PEFT): dataset prep, LoRA training, generation, and adapter save. Runs in under 10 minutes on a 12 GB GPU, and on Apple Silicon via MPS. | | **[Chapter 3: What Data Do I Need?](code/chapter03/README.md)** | Data-quality experiment that trains the same model on four versions of Financial PhraseBank and compares results on a held-out test set; a six-step synthetic data generation pipeline (load → prompt → generate → quality-gate → distribution-check → mix-and-save) using a frontier teacher; and a standalone `DatasetManifest` module for content hashing, lineage tracking, and retention scheduling. | | **[Chapter 4: In-Context Learning and Few-Shot Adaptation](code/chapter04/README.md)** | Few-shot ticket classifier, prompt validator with run-to-run variability measurement, minimal RAG pipeline (50 lines), and a Precision@k / Recall@k / Hit@1 retrieval evaluator. CPU-friendly; GPU optional. | | **[Chapter 5: Parameter-Efficient Fine-Tuning (LoRA and QLoRA)](code/chapter05/README.md)** | LoRA and QLoRA adapters trained on a 400-example Dolly subset of Qwen3-4B-Instruct-2507, evaluated against the base model with per-category Token-F1 and a safety regression suite. | @@ -33,46 +35,64 @@ Every chapter ships with runnable code. The hands-on chapters (4 through 9) repr | **[Chapter 9: Managing Model Evolution, Drift, and Versioning](code/chapter09/README.md)** | A JSON-backed model registry, a TF-IDF drift detector, a simulated rollback workflow, a canary-prompt monitor, and a red-team safety monitor with per-category alerting. | **Start here:** -1. [code/README.md](code/README.md) — set up your Python environment and install the package. +1. [code/README.md](code/README.md): set up your Python environment and install the package. 2. The chapter README for whichever chapter you are reading. ## Quick start +**1. Clone and create a virtual environment** (Python 3.12+): + ```bash git clone https://github.com/bahree/ModelAdaptationBook cd ModelAdaptationBook/code -# Set up Python 3.10+ environment and install PyTorch + the book package. -# Full instructions (including NVIDIA driver install on fresh Ubuntu/Proxmox VMs) -# are in code/README.md. - python3 -m venv .venv source .venv/bin/activate # macOS/Linux # .venv\Scripts\Activate.ps1 # Windows PowerShell python -m pip install -U pip -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126 -pip install -e ".[dev]" +``` -# Smoke-test the chapter 4 code (CPU-friendly, no model download needed): -pytest chapter04/tests/ -v +**2. Install PyTorch for your platform** (pick the one that matches your machine): + +- **NVIDIA GPU (Linux/Windows), CUDA 12.6:** + ```bash + pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126 + ``` +- **macOS (Apple Silicon):** uses the MPS (Metal) backend automatically, no CUDA needed. + ```bash + pip install torch torchvision torchaudio + ``` +- **AMD GPU (Linux, ROCm):** validated on an MI300X (ROCm 7.x). Match the index URL to your ROCm version; the example below is what we tested. + ```bash + pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.0 + ``` +- **CPU only (any platform, no GPU):** + ```bash + pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu + ``` + +For other CUDA versions (12.1, 11.8) or to confirm the right command for your machine, see the official selector at . `code/README.md` has more detail, including NVIDIA driver install steps for fresh Ubuntu/Proxmox VMs. Not sure which accelerator runs which chapter, or how much GPU memory you need? See **[ACCELERATORS.md](ACCELERATORS.md)**. + +**3. Install the book package and smoke-test:** + +```bash +pip install -e ".[dev]" +pytest chapter04/tests/ -v # CPU-friendly, no model download needed ``` After that, follow the chapter README for the chapter you want to run. -## GPU requirements at a glance +## Accelerators and environment + +The full book runs on NVIDIA (CUDA) and AMD (ROCm) GPUs; most of it also runs on Apple Silicon (MPS), and the lightweight chapters run on CPU. **[ACCELERATORS.md](ACCELERATORS.md)** is the complete reference: -| Chapter | Minimum GPU | Recommended | CPU fallback | -|---|---|---|---| -| 4 (ICL/RAG) | None for mock backends; ~8 GB for the optional Qwen3-4B HF backend | 12 GB+ | Yes (mock backend / hash embedder) | -| 5 (LoRA) | 8 GB (RTX 3060/4060+) | 12 GB+ | Yes, but ~20× slower | -| 5 (QLoRA) | 6 GB | 8 GB+ | Not recommended | -| 6 (Full SFT) | 24 GB (A30 / RTX 4090) | A100 40 GB+ | No | -| 7 (Distillation) | 12 GB (LoRA student) + 24 GB to host the chapter 6 teacher | 24 GB+ | Not recommended | -| 8 (DPO) | 24 GB | A100 40 GB+ | No | -| 9 (Drift / Registry / Monitor) | None for the CPU stages (registry, drift detector, rollback demo); ~8 GB for the GPU stages (canary, safety monitor) | 12 GB+ | Yes for stages 1, 2, and 4 | +- **[What runs where](ACCELERATORS.md#what-runs-where)** — a chapter-by-accelerator capability matrix. +- **[GPU requirements at a glance](ACCELERATORS.md#gpu-requirements-at-a-glance)** — per-chapter VRAM needs for NVIDIA, AMD, CPU, and Apple Silicon. +- **[Validated environments](ACCELERATORS.md#validated-environments)** and **[dependency versions](ACCELERATORS.md#dependency-versions)** — the exact machines and package versions we tested. +- **[Performance across GPUs](ACCELERATORS.md#performance-across-gpus)** — A30 vs MI300X vs H200 timings, plus design insights. -**Disk space:** budget about 50 GB free for the Hugging Face model cache plus chapter 6's run directory (full-parameter checkpoints with optimizer state are 22-24 GB each). See `code/chapter06/README.md` for the breakdown. +Two common gotchas, both covered there: chapter 5's QLoRA needs an NVIDIA or AMD GPU ([why](ACCELERATORS.md#why-qlora-needs-an-nvidia-or-amd-gpu)), and the full-parameter chapters (6, 7, 8) need ~24 GB so they do not fit a 16 GB Mac. ## About the book diff --git a/code/README.md b/code/README.md index e6f14c4..340d70e 100644 --- a/code/README.md +++ b/code/README.md @@ -1,258 +1,303 @@ -# Book code workspace - -Welcome to the code repo for the **Model Adaptation** book. This folder contains the runnable code, organized by chapter. - -**Repository**: - -> **Working directory:** All setup and run commands in this README must be executed from the **`code/`** directory—not the parent repo folder. If you cloned the repo and are in the root (e.g. `ModelAdaptationBook`), run `cd code` first. Running `pip install -e .` or other commands from the wrong directory will fail. - -## Where is the Code? - -The book's code is organized by chapter. Every chapter has runnable code now. - -| Folder | Status | Contents | -|---|---|---| -| **`chapter01/`** | **Runnable** | Reproducibility script for the §1.6 sidebar (`run_sidebar_example.py`). Runs the same fictional-policy prompt through base Qwen3-4B, the Chapter 5 LoRA adapter, and the Chapter 6 SFT model. Base-only mode runs without Chapter 5/6 artifacts on disk. | -| **`chapter02/`** | **Runnable** | Unsloth-based fine-tuning quick-start (`ch02-openfinance-finetuning.py`). Reproduces the Dragon LLM open-finance recipe on Qwen3-0.6B end to end: dataset mix from four HF sources, LoRA via TRL's `SFTTrainer`, five evaluation tests, model export. Needs the `chapter02` extra (`pip install -e ".[chapter02]"`) and a CUDA GPU. | -| **`chapter03/`** | **Runnable** | Data-quality experiment (`ch03_data_quality_explore.py` + helpers), six-step synthetic data pipeline (`ch03_synthetic_data_generation.py`), and `DatasetManifest` module (`ch03_datasetmanifest.py`). The experiment needs a GPU and the `chapter03` extra; the synthetic pipeline needs an Anthropic API key; the manifest module is pure stdlib. | -| **`chapter04/`** | **Runnable** | In-context learning, few-shot prompting, prompt validator, minimal RAG pipeline, retrieval-quality eval (Precision@k / Recall@k / Hit@1). CPU-friendly. | -| **`chapter05/`** | **Runnable** | LoRA and QLoRA fine-tuning of Qwen3-4B-Instruct-2507 on a Dolly subset. Train, evaluate, run inference, optional QLoRA. | -| **`chapter06/`** | **Runnable** | Full-parameter SFT on the same base model, with overfit monitoring, three-way base-vs-LoRA-vs-SFT eval, behavioral tests, and a safety regression suite. | -| **`chapter07/`** | **Runnable** | Black-box distillation from the chapter 6 SFT teacher into a LoRA student. Quality filtering, three-way comparison, safety robustness check, and an optional OpenRouter-backed SFT-vs-frontier comparison. | -| **`chapter08/`** | **Runnable** | DPO (Direct Preference Optimization) on the chapter 6 SFT model using TRL's `DPOTrainer`, three-way base-vs-SFT-vs-DPO comparison, safety-after-DPO check. | -| **`chapter09/`** | **Runnable** | Model registry (JSON-backed), TF-IDF drift detector, simulated rollback workflow, canary-prompt monitor, red-team safety monitor with per-category alerting. | - -Shared utilities live in **`common/`** (JSONL I/O, env loading, deterministic seeding, manifest tracking, and an OpenRouter helper used by chapter 7's optional frontier comparison). Install the whole code workspace with `pip install -e .` from this directory. - -For chapter-specific instructions and the listing-to-code map, see each chapter's `README.md`. - -**Recommended Python**: **3.12+** (minimum: 3.10). If you hit install issues, try Python 3.12. - -## One-time setup (Windows/macOS/Linux) - -### 1. Enter the `code/` directory - -Ensure you are in the `code/` folder before running any commands: - -```bash -cd code # if you're in the parent repo (ModelAdaptationBook, etc.) -``` - -### 2. Check Your Python Version - -First, verify you have Python 3.10 or higher: - -```bash -python3 --version -# Example output: Python 3.12.3 -``` - -**If your version is below 3.10**, install a newer Python version before proceeding. - -### Ubuntu/Debian Prerequisites - -**If you're on Ubuntu/Debian**, install the `venv` package first: - -```bash -sudo apt update -sudo apt install python3.12-venv # or python3.10-venv, python3.11-venv depending on your version -``` - -### Create Virtual Environment - -Create a virtual environment **inside `code/`**: - -- Windows (PowerShell): - - `py -3.12 -m venv .venv` (recommended if installed) - - If you have multiple Pythons installed: `py -0p` to list them, then use e.g. `py -3.12 -m venv .venv` - - `./.venv/Scripts/Activate.ps1` -- macOS/Linux: - - `python3 -m venv .venv` - - `source .venv/bin/activate` - -Upgrade pip: - -- `python -m pip install -U pip` - -### Install PyTorch - -**Important:** Install PyTorch separately before installing the book package. Choose the right build for your system. - -**Minimum required:** PyTorch 2.0+ (recommended: PyTorch 2.10+) - -**Step 1: Check if PyTorch is already installed** - -```bash -python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'CUDA available: {torch.cuda.is_available()}')" -``` - -**If PyTorch is installed:** -- **Version 2.0+:** You're good! Skip to "Install the package" section below -- **Version < 2.0:** Upgrade PyTorch (follow Step 2-4 below) - -**If PyTorch is NOT installed** (you'll see an error): -- **Follow Steps 2-4 below** to install it - -**Step 2: Check if you have a GPU and CUDA installed** - -```bash -# Check CUDA version (if NVIDIA GPU installed) -nvidia-smi - -# Or check nvcc version -nvcc --version -``` - -If `nvidia-smi` reports `NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver` but `lspci | grep -i nvidia` does show a GPU, the kernel module is missing. This is common on freshly provisioned Ubuntu cloud VMs and on hypervisors (Proxmox, ESXi) where the GPU is passed through. Install the matching driver from the Ubuntu archives, then reboot: - -```bash -# Consumer GPUs (RTX, GTX): -sudo apt update -sudo apt install -y nvidia-driver-550 - -# Datacenter GPUs (A30, A100, H100, L40): -sudo apt update -sudo apt install -y nvidia-driver-550-server - -sudo reboot -``` - -After the reboot, `nvidia-smi` should list your GPUs. The package version (550 here) controls which CUDA major version the userspace ships; PyTorch's CUDA build does not need to match exactly (see Step 3 note below). - -**Step 3: Choose the right PyTorch build** - -Visit the PyTorch installation page and select your configuration: -- **URL:** -- **Select:** Stable → Linux/Mac/Windows → Pip → Python - -**Common scenarios:** - -- **CUDA 12.6 (most common for Ubuntu 24.04):** - ```bash - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126 - ``` - -- **CUDA 12.1 (common for Ubuntu 22.04/Windows):** - ```bash - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 - ``` - -- **CPU only (testing, no GPU training):** - ```bash - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - ``` - -**Note:** PyTorch CUDA versions don't need to exactly match your system CUDA. For example, PyTorch built for CUDA 12.1 works fine with CUDA 12.4 installed. Choose the closest available version from the PyTorch selector. - -**Step 4: Verify PyTorch installation** - -```bash -python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'CUDA available: {torch.cuda.is_available()}')" -``` - -Expected output: -``` -PyTorch: 2.10.0+cu126 (or similar) -CUDA available: True (should be True if you have a GPU) -``` - -If CUDA shows `False` but you have a GPU, you may have installed the CPU version by mistake. Reinstall PyTorch with the correct CUDA build. - -## Install the package (from the `code/` directory) - -Recommended (author/dev friendly): - -- `pip install -e .` - -Non-editable (fine for readers): - -- `pip install .` - -### What does “editable” (`-e`) mean? - -- **Editable** installs link your environment to your working folder, so code changes take effect immediately. -- **Non-editable** installs copy a snapshot into site-packages; if you edit code locally you must reinstall to see changes. - -Optional extras: - -These are **optional dependency groups** defined in `pyproject.toml`. You can install them with: - -- Dev/test tooling (pytest, ruff): - - `pip install -e ".[dev]"` -- W&B (optional, non-fatal experiment tracking): - - `pip install -e ".[wandb]"` -- QLoRA dependencies (only needed if you want to run **chapter 5's QLoRA step**; bitsandbytes is GPU/platform dependent): - - `pip install -e ".[qlora]"` - -You can combine extras: - -- `pip install -e ".[dev,wandb]"` -- `pip install -e ".[dev,wandb,qlora]"` (only if your environment supports the bitsandbytes wheel) - -**Chapter 7 optional dependency: OpenRouter.** Chapter 7 includes an *optional* script that compares the chapter 6 SFT model against a frontier API (Claude, Gemini, DeepSeek, GPT) via OpenRouter. It uses `requests` (already pulled in by the core dependencies). You only need an OpenRouter API key in `code/.env` to use it; see `chapter07/README.md`. - -## (Optional) Set Hugging Face Token - -**Recommended but not required.** Setting a Hugging Face token enables: -- Higher rate limits for model downloads -- Faster download speeds -- Access to gated models (if needed) - -**To set your token:** - -1. Create a free account at -2. Get your token from -3. Set it in your environment: - -```bash -# Linux/macOS -export HF_TOKEN="hf_..." - -# Windows PowerShell -$env:HF_TOKEN="hf_..." - -# Or add to your shell profile (.bashrc, .zshrc, etc.) -echo 'export HF_TOKEN="hf_..."' >> ~/.bashrc -``` - -**Note:** If you don't set a token, downloads still work but you may see warnings about unauthenticated requests. This is harmless. - -## Getting Started - -Once you've completed the setup above, navigate to the chapter you want to work on. Every chapter from 1 onward ships runnable code. - -- **Chapter 1 (Why model adaptation?)**: `chapter01/README.md` — reproducibility script for the chapter's "What the gap actually looks like" sidebar. Inference-only. -- **Chapter 2 (How to adapt?)**: `chapter02/README.md` — Unsloth-based open-finance fine-tuning quick-start. Needs `pip install -e ".[chapter02]"` and a CUDA GPU. -- **Chapter 3 (What data do I need?)**: `chapter03/README.md` — data-quality experiment, six-step synthetic data pipeline, and dataset manifest module. Needs `pip install -e ".[chapter03]"`; the synthetic pipeline additionally needs `ANTHROPIC_API_KEY`. -- **Chapter 4 (ICL, few-shot, RAG)**: `chapter04/README.md` — few-shot prompting, prompt validator, minimal RAG pipeline, retrieval evaluation. CPU-friendly; GPU optional. -- **Chapter 5 (LoRA & QLoRA)**: `chapter05/README.md` — data preparation, training, evaluation, inference; QLoRA branch optional. -- **Chapter 6 (Full SFT)**: `chapter06/README.md` — full-parameter fine-tuning on a technical-support subset; behavioral tests; safety regression suite. -- **Chapter 7 (Distillation)**: `chapter07/README.md` — chapter 6 SFT model as teacher, LoRA student, three-way evaluation, optional OpenRouter frontier comparison. -- **Chapter 8 (DPO and alignment)**: `chapter08/README.md` — preference data pipeline, DPO training, three-way comparison, safety after DPO. -- **Chapter 9 (Drift / registry / monitor)**: `chapter09/README.md` — model registry, drift detector, rollback demo, canary prompts, safety monitor. - -**Run all chapter commands from this `code/` directory with your virtual environment activated** (e.g. `source .venv/bin/activate` on Linux/macOS, or `.venv\Scripts\activate` on Windows). If you open a new terminal or reconnect via SSH, activate the venv again or you may see "No module named 'chapterNN'". - -Each chapter README contains: -- Prerequisites and GPU requirements -- Step-by-step commands -- Expected output and runtime estimates -- Troubleshooting tips - -**Chapter dependencies between chapters:** chapter 7 uses chapter 6's SFT checkpoint as its teacher; chapter 8 also starts from chapter 6's SFT model; chapter 9 demonstrates the operational layer using artifacts from chapters 5, 6, and 8. Complete chapter 6 before chapter 7 or chapter 8; chapter 9's stages 1, 2, and 4 are self-contained and run on any machine. - -## Notes for Windows (Hugging Face cache) - -- You may see a warning about **symlinks not being supported**. Caching still works; it may just use more disk space. - - To enable symlinks, turn on Windows **Developer Mode** or run as Administrator. - - To silence the warning: set `HF_HUB_DISABLE_SYMLINKS_WARNING=1`. -- You may see a note about **Xet storage** and `hf_xet` not being installed. This is optional; downloads still work. -- You may see non-fatal warnings from Transformers (e.g. deprecations). These should not prevent downloads or runs. - -## Shared conventions across chapters - -- **Module entrypoints**: we run as Python modules from `code/`, e.g. `python -m chapter05.train_lora ...` -- **Artifacts live in chapter folders**: e.g. `chapter05/runs/...` -- **Reproducibility**: scripts write small manifests (dataset subset, adapter metadata, eval reports) -- **Experiment tracking**: W&B is opt-in; scripts must run when it is disabled or not installed. - +# Book code workspace + +Welcome to the code repo for the **Model Adaptation** book. This folder contains the runnable code, organized by chapter. + +**Repository**: + +> **Working directory:** All setup and run commands in this README must be executed from the **`code/`** directory, not the parent repo folder. If you cloned the repo and are in the root (e.g. `ModelAdaptationBook`), run `cd code` first. Running `pip install -e .` or other commands from the wrong directory will fail. + +> **Which hardware do you have?** The full book runs on NVIDIA (CUDA) and AMD (ROCm); Apple Silicon (MPS) runs everything except 4-bit QLoRA and the full-parameter training chapters (6, 7, 8). For the per-chapter capability matrix, GPU memory requirements, validated environments, dependency versions, and performance across GPUs, see **[ACCELERATORS.md](../ACCELERATORS.md)** at the repo root. This README covers Python and PyTorch install; that one covers what runs where. + +## Where is the Code? + +The book's code is organized by chapter. Every chapter has runnable code now. + +| Folder | Status | Contents | +|---|---|---| +| **`chapter01/`** | **Runnable** | Reproducibility script for the §1.6 sidebar (`run_sidebar_example.py`). Runs the same fictional-policy prompt through base Qwen3-4B, the Chapter 5 LoRA adapter, and the Chapter 6 SFT model. Base-only mode runs without Chapter 5/6 artifacts on disk. | +| **`chapter02/`** | **Runnable** | A five-step LoRA fine-tuning quick-start (`quickstart.py`) on Qwen3-4B-Instruct-2507 using a 40-example Dolly subset: dataset prep, LoRA via TRL's `SFTTrainer`, generation, and adapter save. Runs in under 10 minutes on a 12 GB GPU and needs only the base install. Also includes `run_chapter5_adapter.py`, an optional preview that loads the chapter 5 adapter for a base-vs-adapter comparison. | +| **`chapter03/`** | **Runnable** | Data-quality experiment (`ch03_data_quality_explore.py` + helpers), six-step synthetic data pipeline (`ch03_synthetic_data_generation.py`), and `DatasetManifest` module (`ch03_datasetmanifest.py`). The experiment needs a GPU and the `chapter03` extra; the synthetic pipeline needs an Anthropic API key; the manifest module is pure stdlib. | +| **`chapter04/`** | **Runnable** | In-context learning, few-shot prompting, prompt validator, minimal RAG pipeline, retrieval-quality eval (Precision@k / Recall@k / Hit@1). CPU-friendly. | +| **`chapter05/`** | **Runnable** | LoRA and QLoRA fine-tuning of Qwen3-4B-Instruct-2507 on a Dolly subset. Train, evaluate, run inference, optional QLoRA. | +| **`chapter06/`** | **Runnable** | Full-parameter SFT on the same base model, with overfit monitoring, three-way base-vs-LoRA-vs-SFT eval, behavioral tests, and a safety regression suite. | +| **`chapter07/`** | **Runnable** | Black-box distillation from the chapter 6 SFT teacher into a LoRA student. Quality filtering, three-way comparison, safety robustness check, and an optional OpenRouter-backed SFT-vs-frontier comparison. | +| **`chapter08/`** | **Runnable** | DPO (Direct Preference Optimization) on the chapter 6 SFT model using TRL's `DPOTrainer`, three-way base-vs-SFT-vs-DPO comparison, safety-after-DPO check. | +| **`chapter09/`** | **Runnable** | Model registry (JSON-backed), TF-IDF drift detector, simulated rollback workflow, canary-prompt monitor, red-team safety monitor with per-category alerting. | + +Shared utilities live in **`common/`** (JSONL I/O, env loading, deterministic seeding, manifest tracking, and an OpenRouter helper used by chapter 7's optional frontier comparison). Install the whole code workspace with `pip install -e .` from this directory. + +For chapter-specific instructions and the listing-to-code map, see each chapter's `README.md`. + +**Required Python**: **3.12+**. If you hit install issues, confirm `python3 --version` reports 3.12 or higher. + +## One-time setup (Windows/macOS/Linux) + +### 1. Enter the `code/` directory + +Ensure you are in the `code/` folder before running any commands: + +```bash +cd code # if you're in the parent repo (ModelAdaptationBook, etc.) +``` + +### 2. Check Your Python Version + +First, verify you have Python 3.12 or higher: + +```bash +python3 --version +# Example output: Python 3.12.3 +``` + +**If your version is below 3.12**, install a newer Python version before proceeding. + +### Ubuntu/Debian Prerequisites + +**If you're on Ubuntu/Debian**, install the `venv` package first: + +```bash +sudo apt update +sudo apt install python3.12-venv # or python3.10-venv, python3.11-venv depending on your version +``` + +### Create Virtual Environment + +Create a virtual environment **inside `code/`**: + +- Windows (PowerShell): + - `py -3.12 -m venv .venv` (recommended if installed) + - If you have multiple Pythons installed: `py -0p` to list them, then use e.g. `py -3.12 -m venv .venv` + - `./.venv/Scripts/Activate.ps1` +- macOS/Linux: + - `python3 -m venv .venv` + - `source .venv/bin/activate` + +Upgrade pip: + +- `python -m pip install -U pip` + +### Install PyTorch + +**Important:** Install PyTorch separately before installing the book package. Choose the right build for your system. + +**Minimum required:** PyTorch 2.0+ (recommended: PyTorch 2.10+) + +**Step 1: Check if PyTorch is already installed** + +```bash +python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'CUDA available: {torch.cuda.is_available()}')" +``` + +**If PyTorch is installed:** +- **Version 2.0+:** You're good! Skip to "Install the package" section below +- **Version < 2.0:** Upgrade PyTorch (follow Step 2-4 below) + +**If PyTorch is NOT installed** (you'll see an error): +- **Follow Steps 2-4 below** to install it + +**Step 2: Check if you have a GPU and CUDA installed** + +```bash +# Check CUDA version (if NVIDIA GPU installed) +nvidia-smi + +# Or check nvcc version +nvcc --version +``` + +If `nvidia-smi` reports `NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver` but `lspci | grep -i nvidia` does show a GPU, the kernel module is missing. This is common on freshly provisioned Ubuntu cloud VMs and on hypervisors (Proxmox, ESXi) where the GPU is passed through. Install the matching driver from the Ubuntu archives, then reboot: + +```bash +# Consumer GPUs (RTX, GTX): +sudo apt update +sudo apt install -y nvidia-driver-550 + +# Datacenter GPUs (A30, A100, H100, L40): +sudo apt update +sudo apt install -y nvidia-driver-550-server + +sudo reboot +``` + +After the reboot, `nvidia-smi` should list your GPUs. The package version (550 here) controls which CUDA major version the userspace ships; PyTorch's CUDA build does not need to match exactly (see Step 3 note below). + +**Step 3: Choose the right PyTorch build** + +Visit the PyTorch installation page and select your configuration: +- **URL:** +- **Select:** Stable → Linux/Mac/Windows → Pip → Python + +**Common scenarios:** + +- **macOS (Apple Silicon: M1/M2/M3/M4):** no CUDA index URL, no special flags. PyTorch uses Apple's MPS (Metal) backend automatically. + ```bash + pip3 install torch torchvision torchaudio + ``` + After install, verify MPS is available with: + ```bash + python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'MPS available: {torch.backends.mps.is_available()}')" + ``` + See the **macOS compatibility note** at the end of this section for which chapters run on Apple Silicon. + +- **macOS (Intel Macs):** no GPU acceleration; PyTorch installs the CPU build. + ```bash + pip3 install torch torchvision torchaudio + ``` + +- **Linux/Windows with CUDA 12.6 (most common for Ubuntu 24.04):** + ```bash + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126 + ``` + +- **Linux/Windows with CUDA 12.1 (common for Ubuntu 22.04/Windows):** + ```bash + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 + ``` + +- **Linux with an AMD GPU (ROCm):** validated on an AMD Instinct MI300X under ROCm 7.x; the LoRA quickstart, QLoRA (4-bit via `bitsandbytes`), and full-parameter SFT all trained correctly. ROCm is Linux-only and works best on datacenter (MI-series) cards; consumer RDNA support varies by GPU generation. Match the index URL to your ROCm version; the line below is what we tested. + ```bash + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.0 + ``` + On a working ROCm install, `torch.cuda.is_available()` returns `True` (ROCm presents as CUDA in PyTorch), so the chapter preflight checks let you proceed. + +- **CPU only (Linux/Windows, no GPU; testing only):** + ```bash + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu + ``` + +**Note:** PyTorch CUDA versions don't need to exactly match your system CUDA. For example, PyTorch built for CUDA 12.1 works fine with CUDA 12.4 installed. Choose the closest available version from the PyTorch selector. The `--index-url` flag only applies to NVIDIA CUDA builds and the explicit CPU-only build; on macOS the default PyPI wheels are correct. + +**Step 4: Verify PyTorch installation** + +```bash +# Linux/Windows with NVIDIA GPU: +python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'CUDA available: {torch.cuda.is_available()}')" + +# macOS (Apple Silicon): +python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'MPS available: {torch.backends.mps.is_available()}')" +``` + +Expected output (Linux/Windows + NVIDIA): +``` +PyTorch: 2.10.0+cu126 (or similar) +CUDA available: True +``` + +Expected output (macOS + Apple Silicon): +``` +PyTorch: 2.10.0 (or similar; no +cu suffix) +MPS available: True +``` + +If CUDA shows `False` on a Linux/Windows machine that has an NVIDIA GPU, you may have installed the CPU version by mistake. Reinstall PyTorch with the correct CUDA build. If MPS shows `False` on an Apple Silicon Mac, your PyTorch is older than 1.12; upgrade to PyTorch 2.0+. + +### macOS / AMD / what runs where + +The Apple Silicon path uses Apple's MPS backend instead of CUDA; not every chapter has a Mac-friendly code path. AMD (ROCm) runs the full book on a Linux box. For the per-chapter capability matrix (NVIDIA / Apple Silicon / AMD / CPU), GPU memory requirements, validated environments, dependency versions, and performance comparisons, see **[ACCELERATORS.md](../ACCELERATORS.md)** at the repo root. + +Quick rules of thumb covered there: + +- **NVIDIA (CUDA) and AMD (ROCm):** full book, including chapter 5 QLoRA and the full-parameter chapters (6, 7, 8). +- **Apple Silicon (MPS):** chapters 1 (base-only), 2, 3, 4, 5 LoRA, and 9 CPU stages. **Not** chapter 5 QLoRA (`bitsandbytes` is CUDA/ROCm-only) and **not** the full-parameter chapters (they exceed a 16 GB Mac's memory). +- **CPU only:** lightweight chapters (4, 9 CPU stages, mock backends); training chapters are impractical. + +If you only have a Mac, the realistic path for the training chapters is Google Colab or a cloud GPU box. + +## Install the package (from the `code/` directory) + +Recommended (author/dev friendly): + +- `pip install -e .` + +Non-editable (fine for readers): + +- `pip install .` + +### What does “editable” (`-e`) mean? + +- **Editable** installs link your environment to your working folder, so code changes take effect immediately. +- **Non-editable** installs copy a snapshot into site-packages; if you edit code locally you must reinstall to see changes. + +Optional extras: + +These are **optional dependency groups** defined in `pyproject.toml`. You can install them with: + +- Dev/test tooling (pytest, ruff): + - `pip install -e ".[dev]"` +- W&B (optional, non-fatal experiment tracking): + - `pip install -e ".[wandb]"` +- QLoRA dependencies (only needed if you want to run **chapter 5's QLoRA step**; bitsandbytes is GPU/platform dependent): + - `pip install -e ".[qlora]"` + +You can combine extras: + +- `pip install -e ".[dev,wandb]"` +- `pip install -e ".[dev,wandb,qlora]"` (only if your environment supports the bitsandbytes wheel) + +**Chapter 7 optional dependency: OpenRouter.** Chapter 7 includes an *optional* script that compares the chapter 6 SFT model against a frontier API (Claude, Gemini, DeepSeek, GPT) via OpenRouter. It uses `requests` (already pulled in by the core dependencies). You only need an OpenRouter API key in `code/.env` to use it; see `chapter07/README.md`. + +## (Optional) Set Hugging Face Token + +**Recommended but not required.** Setting a Hugging Face token enables: +- Higher rate limits for model downloads +- Faster download speeds +- Access to gated models (if needed) + +**To set your token:** + +1. Create a free account at +2. Get your token from +3. Set it in your environment: + +```bash +# Linux/macOS +export HF_TOKEN="hf_..." + +# Windows PowerShell +$env:HF_TOKEN="hf_..." + +# Or add to your shell profile (.bashrc, .zshrc, etc.) +echo 'export HF_TOKEN="hf_..."' >> ~/.bashrc +``` + +**Note:** If you don't set a token, downloads still work but you may see warnings about unauthenticated requests. This is harmless. + +## Getting Started + +Once you've completed the setup above, navigate to the chapter you want to work on. Every chapter from 1 onward ships runnable code. + +- **Chapter 1 (Why model adaptation?)**: `chapter01/README.md`: reproducibility script for the chapter's "What the gap actually looks like" sidebar. Inference-only. +- **Chapter 2 (How to adapt?)**: `chapter02/README.md`: five-step LoRA fine-tuning quick-start on Qwen3-4B-Instruct-2507. Needs only the base install; runs on a 12 GB GPU (or Apple Silicon MPS). +- **Chapter 3 (What data do I need?)**: `chapter03/README.md`: data-quality experiment, six-step synthetic data pipeline, and dataset manifest module. Needs `pip install -e ".[chapter03]"`; the synthetic pipeline additionally needs `ANTHROPIC_API_KEY`. +- **Chapter 4 (ICL, few-shot, RAG)**: `chapter04/README.md`: few-shot prompting, prompt validator, minimal RAG pipeline, retrieval evaluation. CPU-friendly; GPU optional. +- **Chapter 5 (LoRA & QLoRA)**: `chapter05/README.md`: data preparation, training, evaluation, inference; QLoRA branch optional. +- **Chapter 6 (Full SFT)**: `chapter06/README.md`: full-parameter fine-tuning on a technical-support subset; behavioral tests; safety regression suite. +- **Chapter 7 (Distillation)**: `chapter07/README.md`: chapter 6 SFT model as teacher, LoRA student, three-way evaluation, optional OpenRouter frontier comparison. +- **Chapter 8 (DPO and alignment)**: `chapter08/README.md`: preference data pipeline, DPO training, three-way comparison, safety after DPO. +- **Chapter 9 (Drift / registry / monitor)**: `chapter09/README.md`: model registry, drift detector, rollback demo, canary prompts, safety monitor. + +**Run all chapter commands from this `code/` directory with your virtual environment activated** (e.g. `source .venv/bin/activate` on Linux/macOS, or `.venv\Scripts\activate` on Windows). If you open a new terminal or reconnect via SSH, activate the venv again or you may see "No module named 'chapterNN'". + +Each chapter README contains: +- Prerequisites and GPU requirements +- Step-by-step commands +- Expected output and runtime estimates +- Troubleshooting tips + +**Chapter dependencies between chapters:** chapter 7 uses chapter 6's SFT checkpoint as its teacher; chapter 8 also starts from chapter 6's SFT model; chapter 9 demonstrates the operational layer using artifacts from chapters 5, 6, and 8. Complete chapter 6 before chapter 7 or chapter 8; chapter 9's stages 1, 2, and 4 are self-contained and run on any machine. + +## Notes for Windows (Hugging Face cache) + +- You may see a warning about **symlinks not being supported**. Caching still works; it may just use more disk space. + - To enable symlinks, turn on Windows **Developer Mode** or run as Administrator. + - To silence the warning: set `HF_HUB_DISABLE_SYMLINKS_WARNING=1`. +- You may see a note about **Xet storage** and `hf_xet` not being installed. This is optional; downloads still work. +- You may see non-fatal warnings from Transformers (e.g. deprecations). These should not prevent downloads or runs. + +## Shared conventions across chapters + +- **Module entrypoints**: we run as Python modules from `code/`, e.g. `python -m chapter05.train_lora ...` +- **Artifacts live in chapter folders**: e.g. `chapter05/runs/...` +- **Reproducibility**: scripts write small manifests (dataset subset, adapter metadata, eval reports) +- **Experiment tracking**: W&B is opt-in; scripts must run when it is disabled or not installed. + diff --git a/code/chapter02/README.md b/code/chapter02/README.md index c61d7a3..2108831 100644 --- a/code/chapter02/README.md +++ b/code/chapter02/README.md @@ -1,36 +1,30 @@ # Chapter 2: How to do model adaptation -This folder backs Chapter 2's hands-on quickstart and the optional Dragon-LLM open-finance recipe. The chapter itself is a landscape chapter: it walks the adaptation continuum, the buy-versus-build decision, base model selection, and security considerations. The two pieces of code in this folder play two different roles: +This folder backs Chapter 2's hands-on quickstart. The chapter itself is a landscape chapter: it walks the adaptation continuum, the buy-versus-build decision, base model selection, and security considerations. The two pieces of code in this folder play two roles: 1. **`quickstart.py`** is what chapter 2's section 2.8 walks through: a small five-step LoRA fine-tune on Qwen3-4B-Instruct-2507 using a 40-example subset of Dolly 15K. It runs in under 10 minutes on a 12 GB GPU and lands on the same base model the rest of the book uses, so the adapter it produces is a valid starting point for chapter 5. 2. **`run_chapter5_adapter.py`** is an optional script for curious readers. It loads the chapter 5 LoRA adapter onto the base model and runs the same three demo prompts the quickstart prints, side by side with the base model output. The script resolves the adapter in three tiers: local `chapter05/runs/dolly_lora/` first, then Hugging Face Hub (`bahree/qwen3-4b-dolly-lora-ch5` by default), then optionally the local Ch2 quickstart adapter (only when `--use-quickstart` is passed). Most readers will get the Hub path automatically; readers who have run Ch5 locally get their own copy; readers who only have the quickstart see a clearly-labelled fallback. The script errors with the three resolution paths printed if none is available. -3. **`ch02_openfinance_finetuning.py`** is a bonus reference implementation by Weehyong Tok of the Dragon LLM open-finance recipe (arXiv:2511.08621). It uses Unsloth on `unsloth/Qwen3-0.6B` for a heavier production-style run on a different model family. The chapter prose does not walk this end to end; it is here as supplementary material for readers who want to see a complete Dragon-LLM-style pipeline. **`ch02-openfinance-finetuning.py`** is the original hyphenated copy, kept for compatibility with the Hugging Face short link in the chapter text. ## Assumptions -This README assumes you have completed the one-time setup from [`code/README.md`](../README.md) (Python 3.10+, virtual environment, PyTorch with CUDA). If not, start there first. +This README assumes you have completed the one-time setup from [`code/README.md`](../README.md) (Python 3.12+, virtual environment, PyTorch). If not, start there first. -**The quickstart needs only the base install.** The Dragon-LLM bonus script needs the Chapter 2 extras: +**The quickstart needs only the base install** (no chapter-specific extras): ```bash -# For quickstart.py: nothing beyond the base install pip install -e ".[dev]" - -# For the Dragon-LLM bonus (Unsloth + bitsandbytes): -pip install -e ".[chapter02]" ``` -Unsloth's CUDA kernels do not run on macOS or CPU-only environments. The quickstart in section 2.8 does not use Unsloth, so it runs anywhere PyTorch+CUDA works. +The quickstart uses TRL and PEFT, which are part of the base install, so it runs anywhere PyTorch works, including Apple Silicon via MPS (give it a 16 GB+ Mac; training is slow on unified memory). ## Hardware requirements | Use case | GPU | VRAM | Time on A30 | |---|---|---|---| -| `quickstart.py` (Chapter 2 §2.8) | NVIDIA, CUDA | 12 GB+ | ~8 minutes end to end | -| `ch02_openfinance_finetuning.py` (bonus) | NVIDIA, CUDA | 24 GB+ for Qwen3-0.6B | 15-25 min | +| `quickstart.py` (Chapter 2 §2.8) | NVIDIA, CUDA (or Apple Silicon MPS) | 12 GB+ (16 GB+ unified on Mac) | ~8 minutes end to end | | CPU only | n/a | n/a | Not recommended (hours) | -The book's spine model (used in chapters 4 through 9) is `Qwen/Qwen3-4B-Instruct-2507`. The quickstart uses this model. The bonus Dragon-LLM script uses a different model (`unsloth/Qwen3-0.6B`) because that is what Weehyong's reference implementation was written against; the recipe generalizes to other base models. +The book's spine model (used in chapters 4 through 9) is `Qwen/Qwen3-4B-Instruct-2507`. The quickstart uses this model. ## Running the quickstart (Chapter 2 §2.8) @@ -98,45 +92,21 @@ python chapter05/scripts/publish_adapter.py \ After the push, `chapter02/run_chapter5_adapter.py` resolves to the Hub automatically. -## Running the Dragon-LLM bonus - -```bash -pip install -e ".[chapter02]" -python -m chapter02.ch02_openfinance_finetuning -``` - -This is the underscored, `def main()`-wrapped version of Weehyong's original. The hyphenated original (`ch02-openfinance-finetuning.py`) is also in the folder and runs the same code. - -Expected runtime: 15 to 25 minutes per run on DGX Spark or A100, longer on a 24 GB consumer card. The script downloads four Hugging Face datasets on first run (Financial PhraseBank, Sujet Finance, FiQA, Alpaca) and combines them at the Dragon LLM 54/20/16/8/2 ratio. Outputs land in `chapter02/runs/finance_lora/`. - ## What gets downloaded on first run | Asset | Source | Used by | |---|---|---| | Qwen3-4B-Instruct-2507 | `Qwen/Qwen3-4B-Instruct-2507` | quickstart.py | | Databricks Dolly 15K | `databricks/databricks-dolly-15k` | quickstart.py | -| Qwen3-0.6B (Unsloth mirror) | `unsloth/Qwen3-0.6B` | Dragon-LLM bonus | -| Financial PhraseBank | `takala/financial_phrasebank` | Dragon-LLM bonus | -| Sujet Finance Instruct 177k | `sujet-ai/Sujet-Finance-Instruct-177k` | Dragon-LLM bonus | -| FiQA 2018 | `pauri32/fiqa-2018` | Dragon-LLM bonus | -| Alpaca Cleaned | `yahma/alpaca-cleaned` | Dragon-LLM bonus | -The quickstart's downloads are small (Qwen3-4B is ~8 GB; the Dolly subset filtered down is a handful of MB). The Dragon-LLM bonus downloads several GB of dataset files on first run. +The quickstart's downloads are small: Qwen3-4B is ~8 GB, and the Dolly subset filtered down is a handful of MB. ## Troubleshooting -**`No module named 'unsloth'`** when running the bonus script: run `pip install -e ".[chapter02]"` from the `code/` directory. The quickstart does not need Unsloth. - **CUDA out of memory** in the quickstart: this should not happen on a 12 GB GPU. If it does, set `per_device_train_batch_size=1` (the default) and confirm `gradient_checkpointing_enable()` was called. As a fallback, reduce `max_length` from 512 to 256 in `quickstart.py`. **Slow dataset download** on first run: set `HF_TOKEN` in your environment to use authenticated downloads. -**`fatal error: Python.h: No such file or directory` during training**: Triton's runtime compiler shells out to `gcc` and needs the Python development headers on the include path. Install the matching `python3.X-dev` package (`sudo apt install python3.12-dev` on Ubuntu) and export `CPATH=/usr/include/python3.12` before running the script. - -**`NameError: VARIANT_KWARG_KEYS is not defined`** (Unsloth bonus only): Unsloth's compile-cache generator has not yet caught up with PEFT 0.18+. The book pins `peft<0.18` in `pyproject.toml`; if your environment has a newer PEFT, run `pip install "peft<0.18"` and delete `code/unsloth_compiled_cache/` before retrying. - -**`Can't pickle ` during dataset map** (Unsloth bonus only): triggered by `safetensors>=0.6`. The book pins `safetensors<0.6`; reinstall with `pip install "safetensors<0.6"` if a newer version has crept in. - --- **Repository:** diff --git a/code/chapter02/ch02-openfinance-finetuning.py b/code/chapter02/ch02-openfinance-finetuning.py deleted file mode 100644 index e4cd2f0..0000000 --- a/code/chapter02/ch02-openfinance-finetuning.py +++ /dev/null @@ -1,1474 +0,0 @@ -#!/usr/bin/env python3 -""" -================================================================================ - - BUILDING A FINANCIAL LLM: A COMPLETE TUTORIAL - - How Dragon LLM Turned Qwen 3 into an Open Finance Model - — and how you can replicate it in under an hour - -================================================================================ - -ABOUT THIS TUTORIAL -─────────────────── -In November 2025, a French AI company called Dragon LLM released two open-source -8B-parameter language models fine-tuned for finance. They published their full -methodology in "The LLM Pro Finance Suite" (arXiv:2511.08621), describing how -they curated training data and adapted general-purpose models for financial tasks -like sentiment analysis, regulatory compliance, financial translation, and -retrieval-augmented generation (RAG). - -This tutorial walks through their approach end-to-end. You will: - - Step 1 — Learn why Dragon LLM started from instruction-tuned models - Step 2 — Load a base model with Unsloth for efficient training - Step 3 — Build a training dataset mirroring Dragon LLM's 5-category mix - Step 4 — Format the data for supervised fine-tuning - Step 5 — Train the model - Step 6 — Test the fine-tuned model across financial tasks - Step 7 — Save and export the model - -PREREQUISITES -───────────── - Hardware: NVIDIA GPU with CUDA support (tested on DGX Spark, also works - on Colab T4/L4/A100, RTX 3090/4090, etc.) - Software: Python 3.10+, pip - - Install dependencies before running: - - pip install --upgrade unsloth unsloth_zoo - pip install datasets trl transformers - -ESTIMATED TIME -────────────── - Qwen3-0.6B on DGX Spark: ~15-25 minutes - Qwen3-0.6B on Colab T4: ~20-35 minutes - Qwen3-8B on A100: ~45-60 minutes - -================================================================================ -""" - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 1: WHY THIS APPROACH WORKS ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# Before we write any training code, it is worth understanding the two key -# decisions that define Dragon LLM's methodology. Getting these right matters -# more than any hyperparameter. -# -# -# DECISION 1: START FROM AN INSTRUCTION-TUNED MODEL -# ────────────────────────────────────────────────── -# Most financial LLM projects start from a "base" model — one that has only -# been pre-trained on raw text. It can predict the next token, but it cannot -# follow instructions, hold a conversation, or reason step by step. -# -# Dragon LLM made a different choice. They started from instruction-tuned -# models (Qwen 3 8B, Llama 3.1 8B Instruct). These models have already -# learned, through extensive post-training: -# -# - How to follow complex instructions -# - How to reason step by step -# - Safety and toxicity guardrails -# - Multi-turn conversation skills -# - Tool use and structured output -# -# By starting here, they did not need to teach the model HOW to be helpful. -# They only needed to teach it WHAT finance is. This is far more efficient -# and avoids the catastrophic forgetting problems that plague base-model -# fine-tuning. -# -# In their own words (from the paper): they focused on "enhancing generalist -# instruction-tuned models, leveraging their existing strengths in instruction -# following, reasoning, and toxicity control." -# -# -# DECISION 2: BALANCE THE TRAINING DATA -# ────────────────────────────────────── -# The second insight is about data composition. Naively, you might think: -# "I want a financial model, so I should train only on financial data." -# -# This is a mistake. When you fine-tune a model exclusively on domain data, -# it "forgets" its general capabilities — a phenomenon called catastrophic -# forgetting. The model gets better at finance but worse at everything else, -# including basic reasoning and instruction following. -# -# Dragon LLM's solution was a carefully balanced dataset: -# -# ┌──────────────┬────────┬─────────────────────────────────────────────┐ -# │ Category │ Share │ Purpose │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Financial │ 54% │ Core domain knowledge — sentiment analysis, │ -# │ │ │ regulatory Q&A, financial terminology │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Translation │ 20% │ EN/FR/DE financial document translation — │ -# │ │ │ critical for European finance workflows │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ General │ 16% │ General instruction data — prevents the │ -# │ │ │ model from forgetting how to be helpful │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ RAG │ 8% │ Document-grounded Q&A — teaches the model │ -# │ │ │ to answer from provided context (retrieval) │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Math & Code │ 2% │ Financial math + Python — preserves the │ -# │ │ │ base model's quantitative reasoning │ -# └──────────────┴────────┴─────────────────────────────────────────────┘ -# -# Notice that only 54% is financial data. Nearly half the dataset exists -# purely to PRESERVE capabilities the base model already has. This is the -# price of avoiding catastrophic forgetting, and it is worth paying. -# -# Let's begin building. - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 2: LOADING THE MODEL WITH UNSLOTH ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# We need three things to start training: -# 1. A pre-trained model (our starting point) -# 2. A tokenizer (converts text to numbers and back) -# 3. LoRA adapters (small trainable layers we attach to the frozen model) -# -# WHAT IS UNSLOTH? -# ──────────────── -# Unsloth is a library that makes fine-tuning 2x faster and uses 70% less -# GPU memory compared to standard HuggingFace training. It achieves this -# through custom CUDA kernels, optimized memory management, and intelligent -# gradient checkpointing — all without sacrificing accuracy. -# -# For us, this means we can train on smaller (cheaper) GPUs and finish -# faster. On a DGX Spark, training that would take an hour with vanilla -# HuggingFace finishes in 15–25 minutes with Unsloth. -# -# WHAT IS LoRA? -# ───────────── -# Low-Rank Adaptation (LoRA) is a technique where, instead of updating all -# the model's billions of parameters, we freeze them and attach small -# trainable "adapter" matrices to specific layers. During training, only -# these adapters are updated. -# -# The benefits are significant: -# - Memory: We only store gradients for ~1-2% of parameters -# - Speed: Far fewer computations per training step -# - Storage: The saved adapter is typically 50–200 MB, not 16 GB -# - Composability: You can swap adapters for different tasks -# -# The key hyperparameter is `r` (rank), which controls the adapter size. -# Higher rank = more capacity = more memory. For our 0.6B model, rank 16 -# is sufficient. Dragon LLM likely used rank 32–64 for their 8B models. - -import os -import json -import random -import warnings -from dataclasses import dataclass - -# bitsandbytes uses _check_is_size which is deprecated in torch nightly. -# It's harmless and will be fixed in a future bitsandbytes release. -warnings.filterwarnings("ignore", message="_check_is_size", category=FutureWarning) - -# --------------------------------------------------------------------------- -# 2.1 Configuration -# --------------------------------------------------------------------------- -# We centralize every tunable parameter in a single Config class. This makes -# it easy to experiment: change one value here and the entire pipeline adapts. - -@dataclass -class Config: - # -- Model Selection -- - # We use Qwen3-0.6B, the smallest dense model in the Qwen 3 family. - # Dragon LLM used Qwen3-8B. We use a smaller model for two reasons: - # 1. Faster training (15 min vs. 60 min) - # 2. Lower memory bandwidth demand on DGX Spark - # Once your data pipeline is working, scale up by changing this one line. - model_name: str = "unsloth/Qwen3-0.6B" - - # Maximum sequence length for training. Qwen 3 supports up to 40,960 - # tokens natively (128K with YaRN), but 2048 is enough for our examples - # and keeps memory usage low. - max_seq_length: int = 2048 - - # 4-bit quantization reduces the model's memory footprint by ~4x. - # The model weights are stored in NF4 (NormalFloat 4-bit) format, but - # computations still happen in bf16 for accuracy. This is sometimes - # called "QLoRA" — quantized LoRA. - load_in_4bit: bool = True - - # -- LoRA Hyperparameters -- - # r (rank): Size of the adapter matrices. Higher = more capacity. - # Rule of thumb: r=16 for models ≤2B, r=32 for 4B–8B, r=64 for 70B+ - lora_r: int = 16 - # alpha: Scaling factor. Setting alpha = r is a common default that - # means the adapter's contribution is neither amplified nor dampened. - lora_alpha: int = 16 - # dropout: Regularization. 0.0 is standard for LoRA fine-tuning. - lora_dropout: float = 0.0 - # target_modules: Which layers get adapters. We target all linear - # layers in the transformer — this is what Unsloth recommends and what - # gives the best results. - target_modules: tuple = ( - "q_proj", "k_proj", "v_proj", "o_proj", # attention layers - "gate_proj", "up_proj", "down_proj", # MLP layers - ) - - # -- Training Hyperparameters -- - num_train_epochs: int = 1 # 1 epoch for quick demo; use 2-3 for quality - per_device_train_batch_size: int = 4 # Samples processed per step (per GPU) - gradient_accumulation_steps: int = 4 # Accumulate 4 steps before updating - # ^ Effective batch size = 4 × 4 = 16 samples per weight update - - learning_rate: float = 2e-4 # Standard for LoRA fine-tuning - warmup_ratio: float = 0.05 # Warm up for first 5% of steps - weight_decay: float = 0.01 # Light L2 regularization - lr_scheduler_type: str = "cosine" # Cosine annealing — smooth decay - max_grad_norm: float = 1.0 # Gradient clipping for stability - seed: int = 42 - bf16: bool = True # bfloat16 — supported on Blackwell/Ampere+ - fp16: bool = False - - # -- Dataset Configuration -- - # Dragon LLM used tens of thousands of samples. We use 2000 for speed. - # The ratios below match their published data mix exactly. - total_samples: int = 2000 - finance_ratio: float = 0.54 # 54% financial domain - translation_ratio: float = 0.20 # 20% translation pairs - general_ratio: float = 0.16 # 16% general instruction - rag_ratio: float = 0.08 # 8% document-grounded Q&A - math_code_ratio: float = 0.02 # 2% math and code - - # -- Output -- - output_dir: str = "./qwen-open-finance-output" - - @property - def sample_counts(self): - """Convert ratios to concrete sample counts.""" - return { - "finance": int(self.total_samples * self.finance_ratio), - "translation": int(self.total_samples * self.translation_ratio), - "general": int(self.total_samples * self.general_ratio), - "rag": int(self.total_samples * self.rag_ratio), - "math_code": int(self.total_samples * self.math_code_ratio), - } - - -config = Config() - -print("Tutorial: Fine-Tuning Qwen 3 into a Financial LLM") -print("=" * 55) -print(f"\nModel: {config.model_name}") -print(f"LoRA rank: {config.lora_r}") -print(f"Sequence length: {config.max_seq_length}") -print(f"Total samples: {config.total_samples}") -print(f"Sample counts: {config.sample_counts}") - - -# --------------------------------------------------------------------------- -# 2.2 Load the pre-trained model -# --------------------------------------------------------------------------- -# FastModel.from_pretrained() downloads the model from HuggingFace (first -# run only) and loads it onto the GPU with 4-bit quantization. On the DGX -# Spark, this takes about 30 seconds for 0.6B. The model weights are frozen -# — we will not modify them directly. - -print("\n\n>>> Step 2: Loading the model...") - -from unsloth import FastModel -import torch - -model, tokenizer = FastModel.from_pretrained( - model_name=config.model_name, - max_seq_length=config.max_seq_length, - load_in_4bit=config.load_in_4bit, - load_in_8bit=False, - full_finetuning=False, # We want LoRA, not full fine-tuning -) - - -# --------------------------------------------------------------------------- -# 2.3 Attach LoRA adapters -# --------------------------------------------------------------------------- -# This is where we make the model trainable. get_peft_model() inserts small -# adapter matrices into every layer listed in target_modules. After this -# call, only these adapters will receive gradient updates during training. -# -# use_gradient_checkpointing="unsloth" enables Unsloth's memory-optimized -# version of gradient checkpointing, which trades a small amount of compute -# time for a large reduction in peak memory usage. - -model = FastModel.get_peft_model( - model, - r=config.lora_r, - lora_alpha=config.lora_alpha, - lora_dropout=config.lora_dropout, - target_modules=list(config.target_modules), - use_gradient_checkpointing="unsloth", - random_state=config.seed, -) - -# Let's see how many parameters are actually trainable: -print(f"\nModel loaded: {config.model_name}") -model.print_trainable_parameters() -# You should see something like: "trainable params: 5.5M || all params: 630M || 0.87%" -# This means we are only training ~1% of the model — the rest stays frozen. - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 3: BUILDING THE TRAINING DATASET ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# This is the most important chapter. The quality of your fine-tuned model -# is determined almost entirely by the quality and composition of your data. -# -# Dragon LLM built their dataset through a sophisticated pipeline: -# -# 1. They trained a small classifier called "ClassiFin" (based on -# DeBERTa) to identify financial content in web crawls. -# 2. They ran ClassiFin over the massive FineWeb and FineWeb-2 corpora -# to extract millions of financial documents. -# 3. They supplemented this with Wikipedia articles from financial -# categories (navigating the category graph from seeds like -# "Corporate finance", "Investment", "Banking"). -# 4. They filtered everything for quality using Qwen3-235B as a judge. -# 5. They added translation pairs from OPUS parallel corpora. -# 6. They mixed in general, RAG, and math/code data for balance. -# -# We cannot replicate their full pipeline here (it required a 235B model -# as a quality judge and access to massive web corpora). Instead, we use -# publicly available HuggingFace datasets that cover the same categories -# and task types. The proportions match their published ratios exactly. - -print("\n\n>>> Step 3: Building the training dataset...") - -from datasets import load_dataset, Dataset - -counts = config.sample_counts - - -# --------------------------------------------------------------------------- -# Helper function: format data as chat conversations -# --------------------------------------------------------------------------- -# Every training example must be formatted as a multi-turn conversation in -# the model's expected chat template. Qwen 3 uses a system/user/assistant -# format. This function creates one such conversation. - -def make_chat_message(system: str, user: str, assistant: str) -> dict: - """ - Create a single training example in chat format. - - Args: - system: The system prompt (sets the model's persona/behavior) - user: The user's question or instruction - assistant: The ideal response we want the model to learn - - Returns: - dict with a "messages" key containing the conversation - """ - messages = [] - if system: - messages.append({"role": "system", "content": system}) - messages.append({"role": "user", "content": user}) - messages.append({"role": "assistant", "content": assistant}) - return {"messages": messages} - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.1 FINANCIAL DATA (54% of the dataset) -# ═══════════════════════════════════════════════════════════════════════════ -# This is the core of the dataset. Dragon LLM used financial content -# filtered from web crawls, Wikipedia, and curated Q&A pairs. -# -# We draw from three public datasets that cover the same task types: -# -# • Financial PhraseBank — Sentiment classification of financial news -# sentences. Dragon LLM evaluated their models on this exact benchmark. -# -# • Sujet Finance Instruct — 177K high-quality financial instruction/ -# response pairs covering topics from investment analysis to -# regulatory compliance. -# -# • FiQA — Financial question answering from community forums. -# Dragon LLM also benchmarked on FiQA in their paper. - -print(f"\n [1/5] Financial data ({counts['finance']} samples)...") - -finance_samples = [] - -# --- Source A: Financial PhraseBank (sentiment analysis) --- -# This dataset contains ~4,800 English sentences from financial news, -# each labeled as positive, negative, or neutral by multiple annotators. -# We convert these into instruction-tuning format by asking the model -# to analyze sentiment and explain its reasoning. - -try: - # financial_phrasebank uses a loading script unsupported in datasets>=3.0. - # Download the source zip from the Hub and parse the text file directly. - import zipfile - from huggingface_hub import hf_hub_download - - zip_path = hf_hub_download( - repo_id="takala/financial_phrasebank", - filename="data/FinancialPhraseBank-v1.0.zip", - repo_type="dataset", - ) - fpb = [] - with zipfile.ZipFile(zip_path) as z: - with z.open("FinancialPhraseBank-v1.0/Sentences_AllAgree.txt") as f: - for line in f.read().decode("latin-1").splitlines(): - line = line.strip() - if "@" in line: - sentence, label = line.rsplit("@", 1) - fpb.append({"sentence": sentence.strip(), "label": label.strip()}) - - for row in fpb[:min(len(fpb), counts["finance"] // 3)]: - label = row["label"] - finance_samples.append(make_chat_message( - system=( - "You are a financial analyst specializing in sentiment analysis " - "of financial texts." - ), - user=( - f"Analyze the sentiment of this financial statement and classify " - f"it as positive, negative, or neutral. Explain your reasoning.\n\n" - f"Statement: \"{row['sentence']}\"" - ), - assistant=( - f"Sentiment: **{label}**\n\n" - f"This statement expresses a {label} sentiment. " - + { - "positive": "The language suggests favorable financial performance or outlook.", - "negative": "The language suggests unfavorable financial performance or declining metrics.", - "neutral": "The language is factual and descriptive without expressing a clearly positive or negative outlook.", - }[label] - ), - )) - print(f" Financial PhraseBank: {len(finance_samples)} samples loaded") -except Exception as e: - print(f" Financial PhraseBank: failed ({e})") - - -# --- Source B: Sujet Finance Instruct (financial Q&A) --- -# This is a large instruction-tuning dataset covering a wide range of -# financial topics: investment strategies, risk management, derivatives, -# corporate finance, accounting standards, and more. It provides the -# kind of broad financial knowledge Dragon LLM extracted from curated -# web content. - -try: - sujet = load_dataset("sujet-ai/Sujet-Finance-Instruct-177k", split="train") - n_sujet = min(len(sujet), counts["finance"] // 3) - sujet_subset = sujet.shuffle(seed=config.seed).select(range(n_sujet)) - - for row in sujet_subset: - instruction = row.get("instruction", row.get("input", "")) - output = row.get("output", row.get("response", "")) - if instruction and output: - finance_samples.append(make_chat_message( - system=( - "You are a knowledgeable financial advisor and analyst. " - "Provide accurate, detailed answers to financial questions." - ), - user=instruction, - assistant=output, - )) - print(f" Sujet Finance: {n_sujet} samples loaded") -except Exception as e: - print(f" Sujet Finance: failed ({e})") - - -# --- Source C: FiQA (financial opinion Q&A) --- -# FiQA-2018 contains questions and answers from financial forums and -# communities. These tend to be more conversational and opinion-oriented -# than the structured Sujet data, adding diversity to the training mix. - -try: - fiqa = load_dataset("pauri32/fiqa-2018", split="train") - n_fiqa = min(len(fiqa), counts["finance"] - len(finance_samples)) - fiqa_subset = fiqa.shuffle(seed=config.seed).select(range(max(1, n_fiqa))) - - for row in fiqa_subset: - question = row.get("question", row.get("query", "")) - answer = row.get("answer", row.get("response", "")) - if question and answer: - finance_samples.append(make_chat_message( - system="You are a financial expert. Answer financial questions accurately and concisely.", - user=question, - assistant=str(answer), - )) - print(f" FiQA: {n_fiqa} samples loaded") -except Exception as e: - print(f" FiQA: failed ({e})") - -finance_samples = finance_samples[:counts["finance"]] -print(f" → Total financial: {len(finance_samples)} samples") - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.2 TRANSLATION DATA (20% of the dataset) -# ═══════════════════════════════════════════════════════════════════════════ -# Dragon LLM placed unusual emphasis on translation. Their models support -# English, French, and German — the three most important languages for -# European financial markets. They used OPUS parallel corpora (large -# collections of professionally translated texts) to build translation -# pairs specifically from financial documents. -# -# Why does translation matter for a financial model? Because in European -# finance, professionals routinely work across languages: -# - A KIID (Key Investor Information Document) must be translated into -# every EU language where a fund is marketed -# - Regulatory filings from the ECB are published in multiple languages -# - Cross-border M&A deals involve documents in multiple languages -# -# We create representative financial translation pairs below. In a -# production system, you would source these from OPUS or professional -# translation memories. - -print(f"\n [2/5] Translation data ({counts['translation']} samples)...") - -translation_samples = [] - -# Parallel financial sentences (English ↔ French) -# These represent the kind of content found in annual reports, regulatory -# decisions, fund documentation, and market analysis. - -financial_sentences = [ - ( - "The company reported a 15% increase in quarterly revenue driven by strong demand in emerging markets.", - "La société a enregistré une augmentation de 15 % de son chiffre d'affaires trimestriel grâce à une forte demande sur les marchés émergents.", - ), - ( - "Risk-adjusted returns on the fixed income portfolio exceeded the benchmark by 200 basis points.", - "Les rendements ajustés au risque du portefeuille obligataire ont dépassé l'indice de référence de 200 points de base.", - ), - ( - "The central bank's monetary policy committee voted unanimously to maintain interest rates at current levels.", - "Le comité de politique monétaire de la banque centrale a voté à l'unanimité le maintien des taux d'intérêt à leur niveau actuel.", - ), - ( - "Shareholders approved the proposed merger with a 78% majority vote at the annual general meeting.", - "Les actionnaires ont approuvé la fusion proposée avec une majorité de 78 % lors de l'assemblée générale annuelle.", - ), - ( - "The fund's net asset value declined by 3.2% due to mark-to-market losses on sovereign bond holdings.", - "La valeur liquidative du fonds a diminué de 3,2 % en raison de pertes de valorisation sur les obligations souveraines.", - ), - ( - "Operating expenses were reduced by 12% through operational efficiency improvements and headcount optimization.", - "Les charges d'exploitation ont été réduites de 12 % grâce à l'amélioration de l'efficacité opérationnelle et à l'optimisation des effectifs.", - ), - ( - "The issuer's credit rating was downgraded from A+ to A- following the leveraged buyout announcement.", - "La notation de crédit de l'émetteur a été abaissée de A+ à A- suite à l'annonce du rachat par emprunt.", - ), - ( - "Liquidity coverage ratio remained above regulatory minimums at 135% as of the reporting date.", - "Le ratio de couverture des liquidités est resté au-dessus des minimums réglementaires à 135 % à la date de publication.", - ), - ( - "The PRIIP regulation requires key information documents to be provided to retail investors before purchase.", - "Le règlement PRIIP exige que des documents d'informations clés soient fournis aux investisseurs particuliers avant l'achat.", - ), - ( - "Environmental, social, and governance factors are increasingly integrated into institutional investment decisions.", - "Les facteurs environnementaux, sociaux et de gouvernance sont de plus en plus intégrés dans les décisions d'investissement institutionnel.", - ), - ( - "The swap spread widened significantly amid concerns over counterparty risk in the interbank market.", - "Le spread de swap s'est considérablement élargi en raison des préoccupations liées au risque de contrepartie sur le marché interbancaire.", - ), - ( - "Year-over-year growth in assets under management reached EUR 2.3 billion across all fund strategies.", - "La croissance annuelle des actifs sous gestion a atteint 2,3 milliards d'euros pour l'ensemble des stratégies de fonds.", - ), - ( - "The securitization vehicle issued three tranches of asset-backed securities rated AAA, AA, and BBB.", - "Le véhicule de titrisation a émis trois tranches de titres adossés à des actifs notés AAA, AA et BBB.", - ), - ( - "Regulatory stress tests indicated that Tier 1 capital ratios would remain above 10% under adverse scenarios.", - "Les tests de résistance réglementaires ont indiqué que les ratios de fonds propres de catégorie 1 resteraient supérieurs à 10 % dans les scénarios défavorables.", - ), - ( - "The company's free cash flow conversion rate improved to 92% from 85% in the prior fiscal year.", - "Le taux de conversion du flux de trésorerie disponible de la société s'est amélioré à 92 % contre 85 % l'exercice précédent.", - ), -] - -# We create bidirectional translation pairs (EN→FR and FR→EN), because -# Dragon LLM evaluated on both directions. - -for en, fr in financial_sentences: - # English to French - translation_samples.append(make_chat_message( - system=( - "You are a professional financial translator specializing in " - "English to French translation of financial documents." - ), - user=f"Translate the following financial text from English to French:\n\n\"{en}\"", - assistant=fr, - )) - # French to English - translation_samples.append(make_chat_message( - system=( - "You are a professional financial translator specializing in " - "French to English translation of financial documents." - ), - user=f"Traduisez le texte financier suivant du français vers l'anglais:\n\n\"{fr}\"", - assistant=en, - )) - -# Financial terminology (EN/FR/DE trilingual) -# Dragon LLM also focused on domain-specific terminology accuracy. -financial_terms = [ - ("balance sheet", "bilan", "Bilanz"), - ("income statement", "compte de résultat", "Gewinn- und Verlustrechnung"), - ("cash flow", "flux de trésorerie", "Cashflow"), - ("equity", "capitaux propres", "Eigenkapital"), - ("dividend yield", "rendement du dividende", "Dividendenrendite"), - ("risk assessment", "évaluation des risques", "Risikobewertung"), - ("regulatory compliance", "conformité réglementaire", "Einhaltung gesetzlicher Vorschriften"), - ("market capitalization", "capitalisation boursière", "Marktkapitalisierung"), - ("asset management", "gestion d'actifs", "Vermögensverwaltung"), - ("due diligence", "diligence raisonnable", "Sorgfaltspflicht"), - ("hedge fund", "fonds spéculatif", "Hedgefonds"), - ("initial public offering", "introduction en bourse", "Börsengang"), - ("credit rating", "notation de crédit", "Kreditbewertung"), - ("derivatives trading", "négoce de produits dérivés", "Derivatehandel"), - ("portfolio diversification", "diversification du portefeuille", "Portfoliodiversifikation"), -] - -for en, fr, de in financial_terms: - translation_samples.append(make_chat_message( - system="You are a financial terminology expert fluent in English, French, and German.", - user=f"What is the French and German translation of the financial term '{en}'?", - assistant=( - f"The financial term '{en}' translates to:\n" - f"- **French**: {fr}\n" - f"- **German**: {de}\n\n" - f"These terms are commonly used in financial reporting and regulatory " - f"documents across European markets." - ), - )) - -# Pad with resampled examples if we need more to hit the target count -random.seed(config.seed) -base_count = len(translation_samples) -while len(translation_samples) < counts["translation"]: - translation_samples.append(random.choice(translation_samples[:base_count])) - -translation_samples = translation_samples[:counts["translation"]] -print(f" → Total translation: {len(translation_samples)} samples") - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.3 GENERAL DOMAIN DATA (16% of the dataset) -# ═══════════════════════════════════════════════════════════════════════════ -# This is the "forgetting prevention" slice. Dragon LLM used FineWeb-Edu -# (educational web content) and general instruction data to ensure their -# models stayed good at everyday tasks: summarization, explanation, -# creative writing, general Q&A. -# -# We use the Alpaca dataset — a widely-used collection of ~52K general -# instruction/response pairs. It covers topics from science to history -# to cooking, providing broad diversity. - -print(f"\n [3/5] General domain data ({counts['general']} samples)...") - -general_samples = [] - -try: - alpaca = load_dataset("yahma/alpaca-cleaned", split="train") - n_general = min(len(alpaca), counts["general"]) - alpaca_subset = alpaca.shuffle(seed=config.seed).select(range(n_general)) - - for row in alpaca_subset: - instruction = row["instruction"] - inp = row.get("input", "") - output = row["output"] - user_msg = f"{instruction}\n\n{inp}".strip() if inp else instruction - - general_samples.append(make_chat_message( - system="You are a helpful, accurate, and concise assistant.", - user=user_msg, - assistant=output, - )) - print(f" Alpaca-cleaned: {len(general_samples)} samples loaded") -except Exception as e: - print(f" Alpaca-cleaned: failed ({e})") - -general_samples = general_samples[:counts["general"]] -print(f" → Total general: {len(general_samples)} samples") - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.4 RAG DATA (8% of the dataset) -# ═══════════════════════════════════════════════════════════════════════════ -# RAG (Retrieval-Augmented Generation) is a pattern where you provide a -# document to the model and ask it to answer questions grounded in that -# document — rather than relying on its memorized knowledge. -# -# Dragon LLM explicitly stated their models are "RAG-ready", meaning they -# included substantial RAG training data. This teaches the model to: -# 1. Read a provided context carefully -# 2. Answer only from the given information -# 3. Cite or reference the document -# 4. Say "I don't know" when the answer isn't in the context -# -# In financial applications, RAG is critical. Analysts need models that -# can process a 50-page annual report and answer specific questions about -# it — without hallucinating numbers. -# -# We create representative examples using synthetic financial documents -# (annual reports, ECB decisions, KIIDs, risk assessments). - -print(f"\n [4/5] RAG data ({counts['rag']} samples)...") - -rag_samples = [] - -rag_documents = [ - { - "doc": ( - "Annual Report 2024 — TechCorp Inc.\n\n" - "Revenue for fiscal year 2024 reached $12.4 billion, representing a " - "23% year-over-year increase. Operating income was $3.1 billion with " - "an operating margin of 25%. The company repurchased $2.0 billion in " - "common stock during the year. Research and development expenses " - "increased to $1.8 billion, reflecting continued investment in AI and " - "cloud technologies. Total headcount grew to 45,000 employees across " - "30 countries." - ), - "pairs": [ - ( - "What was TechCorp's revenue in fiscal year 2024?", - "According to the Annual Report 2024, TechCorp Inc. reported " - "revenue of $12.4 billion for fiscal year 2024, which represents " - "a 23% year-over-year increase." - ), - ( - "What was the operating margin?", - "Based on the document, TechCorp's operating margin was 25%, " - "with operating income of $3.1 billion on revenue of $12.4 billion." - ), - ( - "How much did TechCorp spend on R&D?", - "The Annual Report states that research and development expenses " - "increased to $1.8 billion, driven by continued investment in AI " - "and cloud technologies." - ), - ], - }, - { - "doc": ( - "ECB Monetary Policy Decision — March 2024\n\n" - "The Governing Council of the European Central Bank decided to keep " - "the three key ECB interest rates unchanged. The main refinancing rate " - "stands at 4.50%, the marginal lending facility rate at 4.75%, and the " - "deposit facility rate at 4.00%. Inflation has continued to decline but " - "remains above the 2% target. The Governing Council emphasized that " - "future policy decisions will be data-dependent and made on a " - "meeting-by-meeting basis. Economic growth in the euro area remained " - "subdued in Q4 2023." - ), - "pairs": [ - ( - "What are the current ECB interest rates?", - "According to the ECB Monetary Policy Decision, the main " - "refinancing rate is 4.50%, the marginal lending facility rate " - "is 4.75%, and the deposit facility rate is 4.00%." - ), - ( - "What is the ECB's inflation target?", - "The document indicates that while inflation has continued to " - "decline, it remains above the ECB's 2% target." - ), - ], - }, - { - "doc": ( - "KIID — European Growth Equity Fund\n\n" - "Objective: The fund aims to achieve long-term capital growth by " - "investing primarily in European equities. Risk Profile: 5 out of 7 " - "(medium-high risk). The fund invests at least 80% of its assets in " - "shares of companies listed on European stock exchanges. The fund may " - "use financial derivative instruments for hedging purposes. Ongoing " - "charges: 1.45% per annum. Entry charge: up to 5%. Exit charge: none. " - "Performance fee: 15% of outperformance above the STOXX Europe 600 index." - ), - "pairs": [ - ( - "What is the risk profile of this fund?", - "According to the KIID document, the European Growth Equity Fund " - "has a risk profile of 5 out of 7, classified as medium-high risk." - ), - ( - "What are the fund's charges?", - "The KIID states the following charges: ongoing charges of 1.45% " - "per annum, an entry charge of up to 5%, no exit charge, and a " - "performance fee of 15% on outperformance above the STOXX Europe " - "600 index." - ), - ( - "What does the fund invest in?", - "Based on the KIID, the fund invests at least 80% of its assets " - "in shares of companies listed on European stock exchanges, with " - "the objective of achieving long-term capital growth. It may also " - "use financial derivative instruments for hedging purposes." - ), - ], - }, - { - "doc": ( - "Risk Assessment Report — Global Credit Portfolio Q3 2024\n\n" - "The portfolio's Value-at-Risk (VaR) at the 99% confidence level " - "stands at EUR 45 million, a decrease of 8% from Q2. Expected " - "Shortfall (CVaR) is EUR 62 million. Credit exposure to " - "investment-grade issuers represents 72% of the portfolio, while " - "high-yield exposure accounts for 28%. Geographic concentration: " - "45% North America, 35% Europe, 15% Asia-Pacific, 5% Emerging " - "Markets. The portfolio's weighted average credit rating is A-. " - "Duration is 4.2 years. Stress test results show maximum portfolio " - "loss of EUR 180 million under a severe recession scenario." - ), - "pairs": [ - ( - "What is the portfolio's VaR?", - "According to the Risk Assessment Report, the portfolio's " - "Value-at-Risk (VaR) at the 99% confidence level is EUR 45 million, " - "representing an 8% decrease from Q2." - ), - ( - "What is the credit quality breakdown?", - "The report indicates that investment-grade issuers represent 72% " - "of the portfolio, while high-yield exposure accounts for 28%. " - "The weighted average credit rating is A-." - ), - ], - }, -] - -# Convert each document + Q&A pair into a RAG training example -for doc_group in rag_documents: - for question, answer in doc_group["pairs"]: - rag_samples.append(make_chat_message( - system=( - "You are a financial analyst assistant. Answer questions based " - "solely on the provided document. If the information is not in " - "the document, say so." - ), - user=( - f"Based on the following document, answer the question.\n\n" - f"**Document:**\n{doc_group['doc']}\n\n" - f"**Question:** {question}" - ), - assistant=answer, - )) - -# Pad to target count -base_rag = len(rag_samples) -while len(rag_samples) < counts["rag"]: - rag_samples.append(random.choice(rag_samples[:base_rag])) - -rag_samples = rag_samples[:counts["rag"]] -print(f" → Total RAG: {len(rag_samples)} samples") - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.5 MATH & CODE DATA (2% of the dataset) -# ═══════════════════════════════════════════════════════════════════════════ -# The smallest slice, but important. Dragon LLM included math, reasoning, -# and coding data specifically to prevent the base model from losing its -# quantitative skills during fine-tuning. -# -# We focus on financial math (compound interest, bond pricing, portfolio -# returns) and Python code for quantitative finance — tasks that a -# financial LLM should excel at. - -print(f"\n [5/5] Math & code data ({counts['math_code']} samples)...") - -math_code_samples = [] - -# --- Financial math problems with step-by-step solutions --- -math_problems = [ - ( - "Calculate the compound interest on a $10,000 investment at 5% annual rate " - "compounded quarterly for 3 years.", - "Using the compound interest formula: A = P(1 + r/n)^(nt)\n\n" - "Where:\n" - "- P = $10,000 (principal)\n" - "- r = 0.05 (annual rate)\n" - "- n = 4 (quarterly compounding)\n" - "- t = 3 years\n\n" - "A = 10000 × (1 + 0.05/4)^(4×3)\n" - "A = 10000 × (1.0125)^12\n" - "A = 10000 × 1.16075\n" - "A = $11,607.55\n\n" - "Compound interest earned = $11,607.55 - $10,000 = **$1,607.55**" - ), - ( - "A bond has a face value of $1,000, a coupon rate of 6%, and matures in " - "5 years. If the yield to maturity is 4%, what is the bond's price?", - "The bond price is the present value of all future cash flows:\n\n" - "Annual coupon = $1,000 × 6% = $60\n\n" - "Price = Σ [60/(1.04)^t] for t=1 to 5 + 1000/(1.04)^5\n\n" - "= 57.69 + 55.47 + 53.34 + 51.29 + 49.31 + 821.93\n" - "= **$1,089.04**\n\n" - "The bond trades at a premium because the coupon rate (6%) exceeds the " - "yield to maturity (4%)." - ), - ( - "A portfolio has the following weights and returns: Stock A (40%, 12%), " - "Stock B (35%, 8%), Stock C (25%, -3%). Calculate the portfolio return.", - "Portfolio return = Σ (weight × return)\n\n" - "= (0.40 × 12%) + (0.35 × 8%) + (0.25 × -3%)\n" - "= 4.80% + 2.80% + (-0.75%)\n" - "= **6.85%**\n\n" - "The portfolio achieved a positive return of 6.85%, driven primarily by " - "Stock A's strong performance." - ), - ( - "If inflation is 3.2% and the nominal interest rate is 5.5%, what is the " - "real interest rate using the Fisher equation?", - "Using the Fisher equation:\n\n" - "(1 + nominal) = (1 + real) × (1 + inflation)\n\n" - "1.055 = (1 + real) × 1.032\n" - "(1 + real) = 1.055 / 1.032 = 1.02229\n\n" - "Real interest rate = **2.23%**\n\n" - "Note: The simplified approximation (5.5% - 3.2% = 2.3%) gives a close " - "but slightly less accurate result." - ), -] - -# --- Python code for quantitative finance --- -code_problems = [ - ( - "Write a Python function to calculate the Sharpe ratio of a portfolio.", - '```python\n' - 'def sharpe_ratio(portfolio_returns: list, risk_free_rate: float) -> float:\n' - ' """\n' - ' Calculate the Sharpe Ratio of a portfolio.\n' - ' \n' - ' Args:\n' - ' portfolio_returns: List of periodic returns\n' - ' risk_free_rate: Risk-free rate for the same period\n' - ' \n' - ' Returns:\n' - ' Sharpe ratio as a float\n' - ' """\n' - ' import numpy as np\n' - ' \n' - ' returns = np.array(portfolio_returns)\n' - ' excess_returns = returns - risk_free_rate\n' - ' \n' - ' mean_excess = np.mean(excess_returns)\n' - ' std_excess = np.std(excess_returns, ddof=1)\n' - ' \n' - ' if std_excess == 0:\n' - ' return 0.0\n' - ' \n' - ' return mean_excess / std_excess\n' - '\n' - '# Example:\n' - 'returns = [0.05, 0.02, -0.01, 0.08, 0.03]\n' - 'print(f"Sharpe Ratio: {sharpe_ratio(returns, 0.01):.4f}")\n' - '```' - ), - ( - "Write a Python function to calculate the moving average of a stock price series.", - '```python\n' - 'def moving_average(prices: list, window: int) -> list:\n' - ' """\n' - ' Calculate the simple moving average of a price series.\n' - ' \n' - ' Args:\n' - ' prices: List of stock prices\n' - ' window: Number of periods for the moving average\n' - ' \n' - ' Returns:\n' - ' List of moving average values\n' - ' """\n' - ' if len(prices) < window:\n' - ' return []\n' - ' \n' - ' ma = []\n' - ' for i in range(len(prices) - window + 1):\n' - ' avg = sum(prices[i:i + window]) / window\n' - ' ma.append(round(avg, 2))\n' - ' \n' - ' return ma\n' - '\n' - '# Example:\n' - 'prices = [100, 102, 101, 105, 107, 110, 108]\n' - 'print(f"3-day MA: {moving_average(prices, 3)}")\n' - '```' - ), -] - -for q, a in math_problems: - math_code_samples.append(make_chat_message( - system="You are a financial mathematics expert. Show your work step by step.", - user=q, - assistant=a, - )) - -for q, a in code_problems: - math_code_samples.append(make_chat_message( - system="You are a Python developer specializing in quantitative finance. Write clean, well-documented code.", - user=q, - assistant=a, - )) - -# Pad to target count -base_mc = len(math_code_samples) -while len(math_code_samples) < counts["math_code"]: - math_code_samples.append(random.choice(math_code_samples[:base_mc])) - -math_code_samples = math_code_samples[:counts["math_code"]] -print(f" → Total math/code: {len(math_code_samples)} samples") - - -# ═══════════════════════════════════════════════════════════════════════════ -# 3.6 COMBINE AND SHUFFLE -# ═══════════════════════════════════════════════════════════════════════════ -# We concatenate all five categories and shuffle. The shuffle is important: -# if the model sees all financial data first, then all translation data, -# it may forget earlier categories by the end of training. Shuffling -# ensures a balanced exposure throughout. - -print(f"\n Combining all data categories...") - -all_samples = ( - finance_samples + - translation_samples + - general_samples + - rag_samples + - math_code_samples -) - -random.seed(config.seed) -random.shuffle(all_samples) - -dataset = Dataset.from_list(all_samples) - -total = len(all_samples) -print(f"\n Final dataset composition:") -print(f" {'─' * 50}") -print(f" {'Category':<15} {'Count':>6} {'Actual %':>9} {'Target %':>9}") -print(f" {'─' * 50}") -print(f" {'Financial':<15} {len(finance_samples):>6} {len(finance_samples)/total*100:>8.1f}% {config.finance_ratio*100:>8.1f}%") -print(f" {'Translation':<15} {len(translation_samples):>6} {len(translation_samples)/total*100:>8.1f}% {config.translation_ratio*100:>8.1f}%") -print(f" {'General':<15} {len(general_samples):>6} {len(general_samples)/total*100:>8.1f}% {config.general_ratio*100:>8.1f}%") -print(f" {'RAG':<15} {len(rag_samples):>6} {len(rag_samples)/total*100:>8.1f}% {config.rag_ratio*100:>8.1f}%") -print(f" {'Math/Code':<15} {len(math_code_samples):>6} {len(math_code_samples)/total*100:>8.1f}% {config.math_code_ratio*100:>8.1f}%") -print(f" {'─' * 50}") -print(f" {'TOTAL':<15} {total:>6}") - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 4: FORMATTING DATA FOR TRAINING ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# Our dataset currently contains raw conversation dicts. Before training, -# we need to convert these into the exact text format that Qwen 3 expects. -# -# Each model family has its own chat template — special tokens that mark -# where system prompts, user messages, and assistant responses begin and -# end. Qwen 3's template looks roughly like: -# -# <|im_start|>system -# You are a financial analyst...<|im_end|> -# <|im_start|>user -# What is the P/E ratio?<|im_end|> -# <|im_start|>assistant -# The P/E ratio is...<|im_end|> -# -# The tokenizer's apply_chat_template() method handles this formatting -# automatically. We also set enable_thinking=False because Dragon LLM -# trained without the /think reasoning blocks — though the capability -# is preserved in the final model. - -print("\n\n>>> Step 4: Formatting data for training...") - -def apply_chat_template(example): - """Convert a conversation dict into the model's formatted text.""" - text = tokenizer.apply_chat_template( - example["messages"], - tokenize=False, - add_generation_prompt=False, - enable_thinking=False, - ) - return {"text": text} - -dataset = dataset.map(apply_chat_template, num_proc=1) - -# Let's inspect one formatted example to verify it looks right: -print(f"\n Sample formatted example (first 400 chars):") -print(f" {'─' * 60}") -print(f" {dataset[0]['text'][:400]}") -print(f" {'─' * 60}") - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 5: TRAINING ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# We use HuggingFace's SFTTrainer (Supervised Fine-Tuning Trainer) from the -# TRL library, running on top of Unsloth's optimized backend. -# -# KEY TRAINING CONCEPTS -# ───────────────────── -# Effective batch size: -# per_device_batch_size (4) × gradient_accumulation_steps (4) = 16 -# This means the model sees 16 examples before each weight update. -# Larger effective batch sizes give more stable gradients but slower -# iteration. -# -# Learning rate schedule: -# We use cosine annealing with a brief warmup. The learning rate starts -# at 0, rises to 2e-4 during the first 5% of steps, then smoothly -# decays back toward 0 following a cosine curve. This is the most -# common schedule for LoRA fine-tuning. -# -# Packing: -# When packing=True, Unsloth concatenates multiple short examples into -# a single sequence (up to max_seq_length). This eliminates padding -# waste and can speed up training by 30-50% for datasets with many -# short examples (like ours). -# -# bf16 vs fp16: -# bfloat16 has the same range as float32 but lower precision. It is -# more numerically stable than fp16 for training. The DGX Spark's -# Blackwell GPU (and all Ampere+ GPUs) supports bf16 natively. - -print("\n\n>>> Step 5: Training the model...") - -from trl import SFTTrainer, SFTConfig - -# --- Detect GPU capabilities --- -if torch.cuda.is_available(): - gpu_name = torch.cuda.get_device_name(0) - gpu_mem_gb = torch.cuda.get_device_properties(0).total_memory / 1e9 - use_bf16 = torch.cuda.is_bf16_supported() - use_fp16 = not use_bf16 - print(f" GPU detected: {gpu_name}") - print(f" Memory: {gpu_mem_gb:.1f} GB") - print(f" Precision: {'bf16' if use_bf16 else 'fp16'}") -else: - use_bf16 = False - use_fp16 = False - print(" WARNING: No GPU detected. Training will be extremely slow.") - -# --- Configure the trainer --- -training_args = SFTConfig( - output_dir=config.output_dir, - - # How long to train - num_train_epochs=config.num_train_epochs, - - # Batch size and accumulation - per_device_train_batch_size=config.per_device_train_batch_size, - gradient_accumulation_steps=config.gradient_accumulation_steps, - - # Learning rate and schedule - learning_rate=config.learning_rate, - warmup_ratio=config.warmup_ratio, - weight_decay=config.weight_decay, - lr_scheduler_type=config.lr_scheduler_type, - max_grad_norm=config.max_grad_norm, - - # Precision - bf16=use_bf16, - fp16=use_fp16, - - # Logging and checkpoints - logging_steps=10, # Print loss every 10 steps - save_strategy="steps", - save_steps=200, - save_total_limit=2, # Keep only last 2 checkpoints - - # Data handling - max_seq_length=config.max_seq_length, - dataset_text_field="text", - packing=True, # Concatenate short examples for efficiency - - # Reproducibility - seed=config.seed, - report_to="none", # Change to "wandb" for Weights & Biases logging -) - -trainer = SFTTrainer( - model=model, - tokenizer=tokenizer, - train_dataset=dataset, - args=training_args, -) - -# --- Print a summary before we start --- -eff_batch = config.per_device_train_batch_size * config.gradient_accumulation_steps -est_steps = len(dataset) // eff_batch * config.num_train_epochs - -print(f"\n Training plan:") -print(f" {'─' * 45}") -print(f" {'Epochs:':<30} {config.num_train_epochs}") -print(f" {'Batch size (per device):':<30} {config.per_device_train_batch_size}") -print(f" {'Gradient accumulation:':<30} {config.gradient_accumulation_steps}") -print(f" {'Effective batch size:':<30} {eff_batch}") -print(f" {'Estimated steps:':<30} ~{est_steps}") -print(f" {'Learning rate:':<30} {config.learning_rate}") -print(f" {'LoRA rank:':<30} {config.lora_r}") -print(f" {'Packing:':<30} enabled") -print(f" {'─' * 45}") -print(f"\n Starting training... (this will take 15-60 minutes)\n") - -# --- Train! --- -train_result = trainer.train() - -# --- Report results --- -print(f"\n Training complete!") -print(f" {'─' * 45}") -print(f" {'Total steps:':<30} {train_result.global_step}") -print(f" {'Final training loss:':<30} {train_result.training_loss:.4f}") -print(f" {'Runtime:':<30} {train_result.metrics['train_runtime']:.0f} seconds") -print(f" {'Samples/second:':<30} {train_result.metrics['train_samples_per_second']:.2f}") -print(f" {'─' * 45}") - -# A training loss around 1.0-2.0 is typical for a first fine-tuning run. -# Lower is generally better, but very low loss (<0.5) on limited data -# could indicate overfitting. With only 2000 samples, expect ~1.0-1.5. - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 6: TESTING THE FINE-TUNED MODEL ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# Now let's see if the training actually worked. We test one prompt from -# each category in our training mix: -# 1. Financial sentiment analysis -# 2. Financial translation -# 3. RAG (document-grounded Q&A) -# 4. Financial math -# 5. General financial knowledge -# -# Note: with only 2000 training samples and a 0.6B model, do not expect -# GPT-4-level answers. The goal here is to verify that the model has -# learned the FORMAT and STYLE of financial responses. Quality improves -# dramatically when you scale up the data and model size. - -print("\n\n>>> Step 6: Testing the fine-tuned model...") - -def generate_response(prompt, system="You are a financial expert.", max_tokens=256): - """ - Run inference on the fine-tuned model. - - This function: - 1. Formats the prompt using Qwen 3's chat template - 2. Tokenizes and sends it to the GPU - 3. Generates a response using sampling (temperature=0.7) - 4. Decodes and returns the text - """ - messages = [ - {"role": "system", "content": system}, - {"role": "user", "content": prompt}, - ] - - inputs = tokenizer.apply_chat_template( - messages, - tokenize=True, - add_generation_prompt=True, # Add the assistant turn start token - return_tensors="pt", - enable_thinking=False, - ).to(model.device) - - with torch.no_grad(): - outputs = model.generate( - input_ids=inputs, - max_new_tokens=max_tokens, - temperature=0.7, - top_p=0.9, - do_sample=True, - ) - - # Decode only the NEW tokens (skip the input prompt) - response = tokenizer.decode( - outputs[0][inputs.shape[-1]:], - skip_special_tokens=True, - ) - return response - - -# --- Test 1: Financial sentiment analysis --- -print(f"\n Test 1: Financial Sentiment Analysis") -print(f" {'─' * 55}") -prompt = ( - "Analyze the sentiment: 'The company's quarterly earnings " - "exceeded analyst expectations, driven by strong growth in " - "cloud services.'" -) -print(f" Q: {prompt}") -print(f" A: {generate_response(prompt, 'You are a financial sentiment analyst.')}") - -# --- Test 2: Financial translation --- -print(f"\n Test 2: Financial Translation (EN → FR)") -print(f" {'─' * 55}") -prompt = ( - "Translate to French: 'The portfolio's risk-adjusted return " - "outperformed the benchmark index by 150 basis points.'" -) -print(f" Q: {prompt}") -print(f" A: {generate_response(prompt, 'You are a professional financial translator.')}") - -# --- Test 3: RAG / Document-grounded Q&A --- -print(f"\n Test 3: Document-Grounded Q&A (RAG)") -print(f" {'─' * 55}") -prompt = ( - "Based on the following: 'Q3 revenue was €5.2B, up 18% YoY. " - "EBITDA margin improved to 32%.' What was the EBITDA margin?" -) -print(f" Q: {prompt}") -print(f" A: {generate_response(prompt, 'You are a financial analyst. Answer based on the provided information only.')}") - -# --- Test 4: Financial math --- -print(f"\n Test 4: Financial Mathematics") -print(f" {'─' * 55}") -prompt = "If a stock is trading at $50 with a P/E ratio of 25, what are the earnings per share?" -print(f" Q: {prompt}") -print(f" A: {generate_response(prompt, 'You are a financial mathematics expert.')}") - -# --- Test 5: General financial knowledge --- -print(f"\n Test 5: Financial Domain Knowledge") -print(f" {'─' * 55}") -prompt = "What is the difference between a UCITS fund and an AIF under European regulations?" -print(f" Q: {prompt}") -print(f" A: {generate_response(prompt, 'You are a European financial regulatory expert.')}") - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 7: SAVING AND EXPORTING THE MODEL ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# After training, we have a LoRA adapter — a small file (~50-200 MB) that -# modifies the behavior of the base model. We need to save it. -# -# There are several export options: -# -# 1. Save the LoRA adapter only (smallest, requires base model at load) -# 2. Merge adapter into base model and save as 16-bit (standard HF format) -# 3. Export as GGUF for use with llama.cpp / Ollama (local inference) -# 4. Push to HuggingFace Hub (share with the world) -# -# We demonstrate option 1 below. The others are shown as commented code -# you can uncomment when ready. - -print("\n\n>>> Step 7: Saving the model...") - -# --- Option 1: Save LoRA adapter (recommended for development) --- -# This saves only the trained adapter weights, not the full model. -# To use it later, you load the base model + adapter together. -model.save_pretrained(config.output_dir) -tokenizer.save_pretrained(config.output_dir) -print(f" ✓ LoRA adapter saved to: {config.output_dir}/") -print(f" To reload later:") -print(f" >>> model, tokenizer = FastModel.from_pretrained(\"{config.output_dir}\")") - -# --- Option 2: Save merged 16-bit model (full standalone model) --- -# Uncomment to merge the adapter into the base weights and save. -# The result is a standard HuggingFace model that doesn't need Unsloth. -# -# model.save_pretrained_merged( -# f"{config.output_dir}-merged", -# tokenizer, -# save_method="merged_16bit", -# ) -# print(f" ✓ Merged 16-bit model saved to: {config.output_dir}-merged/") - -# --- Option 3: Export as GGUF for llama.cpp / Ollama --- -# GGUF is the format used by llama.cpp and Ollama for local inference. -# q4_k_m is a good balance of quality vs. size. On the DGX Spark, -# the model will run at high speed using the FP4 tensor cores. -# -# model.save_pretrained_gguf( -# f"{config.output_dir}-gguf", -# tokenizer, -# quantization_method="q4_k_m", -# ) -# print(f" ✓ GGUF model saved to: {config.output_dir}-gguf/") -# print(f" Run with: ollama create my-finance-model -f Modelfile") - -# --- Option 4: Push to HuggingFace Hub --- -# Share your model with the community. You'll need a HuggingFace token. -# -# model.push_to_hub("your-username/qwen-open-finance", tokenizer, token="hf_...") -# print(f" ✓ Model pushed to HuggingFace Hub") - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ SUMMARY AND NEXT STEPS ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ - -print(f"\n\n{'═' * 60}") -print(f" TUTORIAL COMPLETE") -print(f"{'═' * 60}") -print(f""" - What you built: - ─────────────── - A financial domain LLM fine-tuned from Qwen 3, following the - same methodology Dragon LLM used to create their Open Finance - models. Your model has been trained on a balanced mix of: - - • Financial sentiment, Q&A, and domain knowledge (54%) - • Financial document translation (EN/FR/DE) (20%) - • General instruction data (catastrophe prevention) (16%) - • Document-grounded RAG examples ( 8%) - • Financial math and Python code ( 2%) - - What to do next: - ──────────────── - 1. SCALE THE DATA - Increase total_samples from 2,000 to 50,000+. This single - change will have the biggest impact on quality. - - 2. SCALE THE MODEL - Change model_name to "unsloth/Qwen3-8B" (what Dragon LLM - used). This fits comfortably on the DGX Spark. - - 3. ADD REAL TRANSLATION DATA - Replace our synthetic pairs with OPUS parallel corpora for - authentic financial document translations. - - 4. USE CLASSIFIN FOR DATA FILTERING - Dragon LLM released their ClassiFin classifier on HuggingFace. - Use it to extract high-quality financial content from FineWeb. - - 5. EVALUATE ON BENCHMARKS - Run the model on FPB, FiQA, FinQA, and ConvFinQA to measure - improvement quantitatively. Dragon LLM's evaluation cookbook - is available at: github.com/Dragon-LLM/llm-open-finance-cookbook - - 6. EXPORT FOR PRODUCTION - Uncomment the GGUF export above and serve with Ollama or - llama.cpp for fast local inference on the DGX Spark. - - References: - ─────────── - Paper: arxiv.org/abs/2511.08621 - Models: huggingface.co/collections/DragonLLM/llm-open-finance - ClassiFin: huggingface.co/DragonLLM/ClassiFin - Cookbook: github.com/Dragon-LLM/llm-open-finance-cookbook - Unsloth: unsloth.ai/docs/models/qwen3-how-to-run-and-fine-tune -""") diff --git a/code/chapter02/ch02_openfinance_finetuning.py b/code/chapter02/ch02_openfinance_finetuning.py deleted file mode 100644 index 970ef98..0000000 --- a/code/chapter02/ch02_openfinance_finetuning.py +++ /dev/null @@ -1,1467 +0,0 @@ -#!/usr/bin/env python3 -""" -================================================================================ - - BUILDING A FINANCIAL LLM: A COMPLETE TUTORIAL - (book-conventions-aligned version of Weehyong's reference implementation) - - Original at: code/chapter02/ch02-openfinance-finetuning.py - https://github.com/weehyong/LLM-finetuning/blob/main/ch02-openfinance-finetuning.py - - This file is identical in logic to the original. It differs only in: - - Filename uses underscores so `python -m chapter02.ch02_openfinance_finetuning` - works without shell quoting. - - Top-level executable code is wrapped in def main() so the module is - importable without triggering training. Functions defined inside the - original's executable flow (make_chat_message, apply_chat_template, - generate_response) become nested functions inside main(); they - capture model/tokenizer via closure. - - Output path now follows the book convention - (chapter02/runs/finance_lora/ instead of ./qwen-open-finance-output). - - All other prose (the seven-step tutorial, the design-decision rationale, - the dataset notes) is preserved verbatim from Weehyong's original. - -================================================================================ - -================================================================================ -""" - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 1: WHY THIS APPROACH WORKS ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# Before we write any training code, it is worth understanding the two key -# decisions that define Dragon LLM's methodology. Getting these right matters -# more than any hyperparameter. -# -# -# DECISION 1: START FROM AN INSTRUCTION-TUNED MODEL -# ────────────────────────────────────────────────── -# Most financial LLM projects start from a "base" model — one that has only -# been pre-trained on raw text. It can predict the next token, but it cannot -# follow instructions, hold a conversation, or reason step by step. -# -# Dragon LLM made a different choice. They started from instruction-tuned -# models (Qwen 3 8B, Llama 3.1 8B Instruct). These models have already -# learned, through extensive post-training: -# -# - How to follow complex instructions -# - How to reason step by step -# - Safety and toxicity guardrails -# - Multi-turn conversation skills -# - Tool use and structured output -# -# By starting here, they did not need to teach the model HOW to be helpful. -# They only needed to teach it WHAT finance is. This is far more efficient -# and avoids the catastrophic forgetting problems that plague base-model -# fine-tuning. -# -# In their own words (from the paper): they focused on "enhancing generalist -# instruction-tuned models, leveraging their existing strengths in instruction -# following, reasoning, and toxicity control." -# -# -# DECISION 2: BALANCE THE TRAINING DATA -# ────────────────────────────────────── -# The second insight is about data composition. Naively, you might think: -# "I want a financial model, so I should train only on financial data." -# -# This is a mistake. When you fine-tune a model exclusively on domain data, -# it "forgets" its general capabilities — a phenomenon called catastrophic -# forgetting. The model gets better at finance but worse at everything else, -# including basic reasoning and instruction following. -# -# Dragon LLM's solution was a carefully balanced dataset: -# -# ┌──────────────┬────────┬─────────────────────────────────────────────┐ -# │ Category │ Share │ Purpose │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Financial │ 54% │ Core domain knowledge — sentiment analysis, │ -# │ │ │ regulatory Q&A, financial terminology │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Translation │ 20% │ EN/FR/DE financial document translation — │ -# │ │ │ critical for European finance workflows │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ General │ 16% │ General instruction data — prevents the │ -# │ │ │ model from forgetting how to be helpful │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ RAG │ 8% │ Document-grounded Q&A — teaches the model │ -# │ │ │ to answer from provided context (retrieval) │ -# ├──────────────┼────────┼─────────────────────────────────────────────┤ -# │ Math & Code │ 2% │ Financial math + Python — preserves the │ -# │ │ │ base model's quantitative reasoning │ -# └──────────────┴────────┴─────────────────────────────────────────────┘ -# -# Notice that only 54% is financial data. Nearly half the dataset exists -# purely to PRESERVE capabilities the base model already has. This is the -# price of avoiding catastrophic forgetting, and it is worth paying. -# -# Let's begin building. - - -# ╔════════════════════════════════════════════════════════════════════════════╗ -# ║ ║ -# ║ STEP 2: LOADING THE MODEL WITH UNSLOTH ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════════╝ -# -# We need three things to start training: -# 1. A pre-trained model (our starting point) -# 2. A tokenizer (converts text to numbers and back) -# 3. LoRA adapters (small trainable layers we attach to the frozen model) -# -# WHAT IS UNSLOTH? -# ──────────────── -# Unsloth is a library that makes fine-tuning 2x faster and uses 70% less -# GPU memory compared to standard HuggingFace training. It achieves this -# through custom CUDA kernels, optimized memory management, and intelligent -# gradient checkpointing — all without sacrificing accuracy. -# -# For us, this means we can train on smaller (cheaper) GPUs and finish -# faster. On a DGX Spark, training that would take an hour with vanilla -# HuggingFace finishes in 15–25 minutes with Unsloth. -# -# WHAT IS LoRA? -# ───────────── -# Low-Rank Adaptation (LoRA) is a technique where, instead of updating all -# the model's billions of parameters, we freeze them and attach small -# trainable "adapter" matrices to specific layers. During training, only -# these adapters are updated. -# -# The benefits are significant: -# - Memory: We only store gradients for ~1-2% of parameters -# - Speed: Far fewer computations per training step -# - Storage: The saved adapter is typically 50–200 MB, not 16 GB -# - Composability: You can swap adapters for different tasks -# -# The key hyperparameter is `r` (rank), which controls the adapter size. -# Higher rank = more capacity = more memory. For our 0.6B model, rank 16 -# is sufficient. Dragon LLM likely used rank 32–64 for their 8B models. - -import os -import json -import random -import warnings -from dataclasses import dataclass - -# bitsandbytes uses _check_is_size which is deprecated in torch nightly. -# It's harmless and will be fixed in a future bitsandbytes release. -warnings.filterwarnings("ignore", message="_check_is_size", category=FutureWarning) - -# --------------------------------------------------------------------------- -# 2.1 Configuration -# --------------------------------------------------------------------------- -# We centralize every tunable parameter in a single Config class. This makes -# it easy to experiment: change one value here and the entire pipeline adapts. - -@dataclass -class Config: - # -- Model Selection -- - # We use Qwen3-0.6B, the smallest dense model in the Qwen 3 family. - # Dragon LLM used Qwen3-8B. We use a smaller model for two reasons: - # 1. Faster training (15 min vs. 60 min) - # 2. Lower memory bandwidth demand on DGX Spark - # Once your data pipeline is working, scale up by changing this one line. - model_name: str = "unsloth/Qwen3-0.6B" - - # Maximum sequence length for training. Qwen 3 supports up to 40,960 - # tokens natively (128K with YaRN), but 2048 is enough for our examples - # and keeps memory usage low. - max_seq_length: int = 2048 - - # 4-bit quantization reduces the model's memory footprint by ~4x. - # The model weights are stored in NF4 (NormalFloat 4-bit) format, but - # computations still happen in bf16 for accuracy. This is sometimes - # called "QLoRA" — quantized LoRA. - load_in_4bit: bool = True - - # -- LoRA Hyperparameters -- - # r (rank): Size of the adapter matrices. Higher = more capacity. - # Rule of thumb: r=16 for models ≤2B, r=32 for 4B–8B, r=64 for 70B+ - lora_r: int = 16 - # alpha: Scaling factor. Setting alpha = r is a common default that - # means the adapter's contribution is neither amplified nor dampened. - lora_alpha: int = 16 - # dropout: Regularization. 0.0 is standard for LoRA fine-tuning. - lora_dropout: float = 0.0 - # target_modules: Which layers get adapters. We target all linear - # layers in the transformer — this is what Unsloth recommends and what - # gives the best results. - target_modules: tuple = ( - "q_proj", "k_proj", "v_proj", "o_proj", # attention layers - "gate_proj", "up_proj", "down_proj", # MLP layers - ) - - # -- Training Hyperparameters -- - num_train_epochs: int = 1 # 1 epoch for quick demo; use 2-3 for quality - per_device_train_batch_size: int = 4 # Samples processed per step (per GPU) - gradient_accumulation_steps: int = 4 # Accumulate 4 steps before updating - # ^ Effective batch size = 4 × 4 = 16 samples per weight update - - learning_rate: float = 2e-4 # Standard for LoRA fine-tuning - warmup_ratio: float = 0.05 # Warm up for first 5% of steps - weight_decay: float = 0.01 # Light L2 regularization - lr_scheduler_type: str = "cosine" # Cosine annealing — smooth decay - max_grad_norm: float = 1.0 # Gradient clipping for stability - seed: int = 42 - bf16: bool = True # bfloat16 — supported on Blackwell/Ampere+ - fp16: bool = False - - # -- Dataset Configuration -- - # Dragon LLM used tens of thousands of samples. We use 2000 for speed. - # The ratios below match their published data mix exactly. - total_samples: int = 2000 - finance_ratio: float = 0.54 # 54% financial domain - translation_ratio: float = 0.20 # 20% translation pairs - general_ratio: float = 0.16 # 16% general instruction - rag_ratio: float = 0.08 # 8% document-grounded Q&A - math_code_ratio: float = 0.02 # 2% math and code - - # -- Output -- - output_dir: str = "chapter02/runs/finance_lora" - - @property - def sample_counts(self): - """Convert ratios to concrete sample counts.""" - return { - "finance": int(self.total_samples * self.finance_ratio), - "translation": int(self.total_samples * self.translation_ratio), - "general": int(self.total_samples * self.general_ratio), - "rag": int(self.total_samples * self.rag_ratio), - "math_code": int(self.total_samples * self.math_code_ratio), - } - - - - -def main(): - """Run the full Open Finance fine-tuning recipe end to end. - - See module docstring for what this does. Run with: - - python -m chapter02.ch02_openfinance_finetuning - - Requires CUDA. See code/chapter02/README.md for the install step. - """ - config = Config() - - print("Tutorial: Fine-Tuning Qwen 3 into a Financial LLM") - print("=" * 55) - print(f"\nModel: {config.model_name}") - print(f"LoRA rank: {config.lora_r}") - print(f"Sequence length: {config.max_seq_length}") - print(f"Total samples: {config.total_samples}") - print(f"Sample counts: {config.sample_counts}") - - - # --------------------------------------------------------------------------- - # 2.2 Load the pre-trained model - # --------------------------------------------------------------------------- - # FastModel.from_pretrained() downloads the model from HuggingFace (first - # run only) and loads it onto the GPU with 4-bit quantization. On the DGX - # Spark, this takes about 30 seconds for 0.6B. The model weights are frozen - # — we will not modify them directly. - - print("\n\n>>> Step 2: Loading the model...") - - from unsloth import FastModel - import torch - - model, tokenizer = FastModel.from_pretrained( - model_name=config.model_name, - max_seq_length=config.max_seq_length, - load_in_4bit=config.load_in_4bit, - load_in_8bit=False, - full_finetuning=False, # We want LoRA, not full fine-tuning - ) - - - # --------------------------------------------------------------------------- - # 2.3 Attach LoRA adapters - # --------------------------------------------------------------------------- - # This is where we make the model trainable. get_peft_model() inserts small - # adapter matrices into every layer listed in target_modules. After this - # call, only these adapters will receive gradient updates during training. - # - # use_gradient_checkpointing="unsloth" enables Unsloth's memory-optimized - # version of gradient checkpointing, which trades a small amount of compute - # time for a large reduction in peak memory usage. - - model = FastModel.get_peft_model( - model, - r=config.lora_r, - lora_alpha=config.lora_alpha, - lora_dropout=config.lora_dropout, - target_modules=list(config.target_modules), - use_gradient_checkpointing="unsloth", - random_state=config.seed, - ) - - # Let's see how many parameters are actually trainable: - print(f"\nModel loaded: {config.model_name}") - model.print_trainable_parameters() - # You should see something like: "trainable params: 5.5M || all params: 630M || 0.87%" - # This means we are only training ~1% of the model — the rest stays frozen. - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ STEP 3: BUILDING THE TRAINING DATASET ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - # - # This is the most important chapter. The quality of your fine-tuned model - # is determined almost entirely by the quality and composition of your data. - # - # Dragon LLM built their dataset through a sophisticated pipeline: - # - # 1. They trained a small classifier called "ClassiFin" (based on - # DeBERTa) to identify financial content in web crawls. - # 2. They ran ClassiFin over the massive FineWeb and FineWeb-2 corpora - # to extract millions of financial documents. - # 3. They supplemented this with Wikipedia articles from financial - # categories (navigating the category graph from seeds like - # "Corporate finance", "Investment", "Banking"). - # 4. They filtered everything for quality using Qwen3-235B as a judge. - # 5. They added translation pairs from OPUS parallel corpora. - # 6. They mixed in general, RAG, and math/code data for balance. - # - # We cannot replicate their full pipeline here (it required a 235B model - # as a quality judge and access to massive web corpora). Instead, we use - # publicly available HuggingFace datasets that cover the same categories - # and task types. The proportions match their published ratios exactly. - - print("\n\n>>> Step 3: Building the training dataset...") - - from datasets import load_dataset, Dataset - - counts = config.sample_counts - - - # --------------------------------------------------------------------------- - # Helper function: format data as chat conversations - # --------------------------------------------------------------------------- - # Every training example must be formatted as a multi-turn conversation in - # the model's expected chat template. Qwen 3 uses a system/user/assistant - # format. This function creates one such conversation. - - def make_chat_message(system: str, user: str, assistant: str) -> dict: - """ - Create a single training example in chat format. - - Args: - system: The system prompt (sets the model's persona/behavior) - user: The user's question or instruction - assistant: The ideal response we want the model to learn - - Returns: - dict with a "messages" key containing the conversation - """ - messages = [] - if system: - messages.append({"role": "system", "content": system}) - messages.append({"role": "user", "content": user}) - messages.append({"role": "assistant", "content": assistant}) - return {"messages": messages} - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.1 FINANCIAL DATA (54% of the dataset) - # ═══════════════════════════════════════════════════════════════════════════ - # This is the core of the dataset. Dragon LLM used financial content - # filtered from web crawls, Wikipedia, and curated Q&A pairs. - # - # We draw from three public datasets that cover the same task types: - # - # • Financial PhraseBank — Sentiment classification of financial news - # sentences. Dragon LLM evaluated their models on this exact benchmark. - # - # • Sujet Finance Instruct — 177K high-quality financial instruction/ - # response pairs covering topics from investment analysis to - # regulatory compliance. - # - # • FiQA — Financial question answering from community forums. - # Dragon LLM also benchmarked on FiQA in their paper. - - print(f"\n [1/5] Financial data ({counts['finance']} samples)...") - - finance_samples = [] - - # --- Source A: Financial PhraseBank (sentiment analysis) --- - # This dataset contains ~4,800 English sentences from financial news, - # each labeled as positive, negative, or neutral by multiple annotators. - # We convert these into instruction-tuning format by asking the model - # to analyze sentiment and explain its reasoning. - - try: - # financial_phrasebank uses a loading script unsupported in datasets>=3.0. - # Download the source zip from the Hub and parse the text file directly. - import zipfile - from huggingface_hub import hf_hub_download - - zip_path = hf_hub_download( - repo_id="takala/financial_phrasebank", - filename="data/FinancialPhraseBank-v1.0.zip", - repo_type="dataset", - ) - fpb = [] - with zipfile.ZipFile(zip_path) as z: - with z.open("FinancialPhraseBank-v1.0/Sentences_AllAgree.txt") as f: - for line in f.read().decode("latin-1").splitlines(): - line = line.strip() - if "@" in line: - sentence, label = line.rsplit("@", 1) - fpb.append({"sentence": sentence.strip(), "label": label.strip()}) - - for row in fpb[:min(len(fpb), counts["finance"] // 3)]: - label = row["label"] - finance_samples.append(make_chat_message( - system=( - "You are a financial analyst specializing in sentiment analysis " - "of financial texts." - ), - user=( - f"Analyze the sentiment of this financial statement and classify " - f"it as positive, negative, or neutral. Explain your reasoning.\n\n" - f"Statement: \"{row['sentence']}\"" - ), - assistant=( - f"Sentiment: **{label}**\n\n" - f"This statement expresses a {label} sentiment. " - + { - "positive": "The language suggests favorable financial performance or outlook.", - "negative": "The language suggests unfavorable financial performance or declining metrics.", - "neutral": "The language is factual and descriptive without expressing a clearly positive or negative outlook.", - }[label] - ), - )) - print(f" Financial PhraseBank: {len(finance_samples)} samples loaded") - except Exception as e: - print(f" Financial PhraseBank: failed ({e})") - - - # --- Source B: Sujet Finance Instruct (financial Q&A) --- - # This is a large instruction-tuning dataset covering a wide range of - # financial topics: investment strategies, risk management, derivatives, - # corporate finance, accounting standards, and more. It provides the - # kind of broad financial knowledge Dragon LLM extracted from curated - # web content. - - try: - sujet = load_dataset("sujet-ai/Sujet-Finance-Instruct-177k", split="train") - n_sujet = min(len(sujet), counts["finance"] // 3) - sujet_subset = sujet.shuffle(seed=config.seed).select(range(n_sujet)) - - for row in sujet_subset: - instruction = row.get("instruction", row.get("input", "")) - output = row.get("output", row.get("response", "")) - if instruction and output: - finance_samples.append(make_chat_message( - system=( - "You are a knowledgeable financial advisor and analyst. " - "Provide accurate, detailed answers to financial questions." - ), - user=instruction, - assistant=output, - )) - print(f" Sujet Finance: {n_sujet} samples loaded") - except Exception as e: - print(f" Sujet Finance: failed ({e})") - - - # --- Source C: FiQA (financial opinion Q&A) --- - # FiQA-2018 contains questions and answers from financial forums and - # communities. These tend to be more conversational and opinion-oriented - # than the structured Sujet data, adding diversity to the training mix. - - try: - fiqa = load_dataset("pauri32/fiqa-2018", split="train") - n_fiqa = min(len(fiqa), counts["finance"] - len(finance_samples)) - fiqa_subset = fiqa.shuffle(seed=config.seed).select(range(max(1, n_fiqa))) - - for row in fiqa_subset: - question = row.get("question", row.get("query", "")) - answer = row.get("answer", row.get("response", "")) - if question and answer: - finance_samples.append(make_chat_message( - system="You are a financial expert. Answer financial questions accurately and concisely.", - user=question, - assistant=str(answer), - )) - print(f" FiQA: {n_fiqa} samples loaded") - except Exception as e: - print(f" FiQA: failed ({e})") - - finance_samples = finance_samples[:counts["finance"]] - print(f" → Total financial: {len(finance_samples)} samples") - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.2 TRANSLATION DATA (20% of the dataset) - # ═══════════════════════════════════════════════════════════════════════════ - # Dragon LLM placed unusual emphasis on translation. Their models support - # English, French, and German — the three most important languages for - # European financial markets. They used OPUS parallel corpora (large - # collections of professionally translated texts) to build translation - # pairs specifically from financial documents. - # - # Why does translation matter for a financial model? Because in European - # finance, professionals routinely work across languages: - # - A KIID (Key Investor Information Document) must be translated into - # every EU language where a fund is marketed - # - Regulatory filings from the ECB are published in multiple languages - # - Cross-border M&A deals involve documents in multiple languages - # - # We create representative financial translation pairs below. In a - # production system, you would source these from OPUS or professional - # translation memories. - - print(f"\n [2/5] Translation data ({counts['translation']} samples)...") - - translation_samples = [] - - # Parallel financial sentences (English ↔ French) - # These represent the kind of content found in annual reports, regulatory - # decisions, fund documentation, and market analysis. - - financial_sentences = [ - ( - "The company reported a 15% increase in quarterly revenue driven by strong demand in emerging markets.", - "La société a enregistré une augmentation de 15 % de son chiffre d'affaires trimestriel grâce à une forte demande sur les marchés émergents.", - ), - ( - "Risk-adjusted returns on the fixed income portfolio exceeded the benchmark by 200 basis points.", - "Les rendements ajustés au risque du portefeuille obligataire ont dépassé l'indice de référence de 200 points de base.", - ), - ( - "The central bank's monetary policy committee voted unanimously to maintain interest rates at current levels.", - "Le comité de politique monétaire de la banque centrale a voté à l'unanimité le maintien des taux d'intérêt à leur niveau actuel.", - ), - ( - "Shareholders approved the proposed merger with a 78% majority vote at the annual general meeting.", - "Les actionnaires ont approuvé la fusion proposée avec une majorité de 78 % lors de l'assemblée générale annuelle.", - ), - ( - "The fund's net asset value declined by 3.2% due to mark-to-market losses on sovereign bond holdings.", - "La valeur liquidative du fonds a diminué de 3,2 % en raison de pertes de valorisation sur les obligations souveraines.", - ), - ( - "Operating expenses were reduced by 12% through operational efficiency improvements and headcount optimization.", - "Les charges d'exploitation ont été réduites de 12 % grâce à l'amélioration de l'efficacité opérationnelle et à l'optimisation des effectifs.", - ), - ( - "The issuer's credit rating was downgraded from A+ to A- following the leveraged buyout announcement.", - "La notation de crédit de l'émetteur a été abaissée de A+ à A- suite à l'annonce du rachat par emprunt.", - ), - ( - "Liquidity coverage ratio remained above regulatory minimums at 135% as of the reporting date.", - "Le ratio de couverture des liquidités est resté au-dessus des minimums réglementaires à 135 % à la date de publication.", - ), - ( - "The PRIIP regulation requires key information documents to be provided to retail investors before purchase.", - "Le règlement PRIIP exige que des documents d'informations clés soient fournis aux investisseurs particuliers avant l'achat.", - ), - ( - "Environmental, social, and governance factors are increasingly integrated into institutional investment decisions.", - "Les facteurs environnementaux, sociaux et de gouvernance sont de plus en plus intégrés dans les décisions d'investissement institutionnel.", - ), - ( - "The swap spread widened significantly amid concerns over counterparty risk in the interbank market.", - "Le spread de swap s'est considérablement élargi en raison des préoccupations liées au risque de contrepartie sur le marché interbancaire.", - ), - ( - "Year-over-year growth in assets under management reached EUR 2.3 billion across all fund strategies.", - "La croissance annuelle des actifs sous gestion a atteint 2,3 milliards d'euros pour l'ensemble des stratégies de fonds.", - ), - ( - "The securitization vehicle issued three tranches of asset-backed securities rated AAA, AA, and BBB.", - "Le véhicule de titrisation a émis trois tranches de titres adossés à des actifs notés AAA, AA et BBB.", - ), - ( - "Regulatory stress tests indicated that Tier 1 capital ratios would remain above 10% under adverse scenarios.", - "Les tests de résistance réglementaires ont indiqué que les ratios de fonds propres de catégorie 1 resteraient supérieurs à 10 % dans les scénarios défavorables.", - ), - ( - "The company's free cash flow conversion rate improved to 92% from 85% in the prior fiscal year.", - "Le taux de conversion du flux de trésorerie disponible de la société s'est amélioré à 92 % contre 85 % l'exercice précédent.", - ), - ] - - # We create bidirectional translation pairs (EN→FR and FR→EN), because - # Dragon LLM evaluated on both directions. - - for en, fr in financial_sentences: - # English to French - translation_samples.append(make_chat_message( - system=( - "You are a professional financial translator specializing in " - "English to French translation of financial documents." - ), - user=f"Translate the following financial text from English to French:\n\n\"{en}\"", - assistant=fr, - )) - # French to English - translation_samples.append(make_chat_message( - system=( - "You are a professional financial translator specializing in " - "French to English translation of financial documents." - ), - user=f"Traduisez le texte financier suivant du français vers l'anglais:\n\n\"{fr}\"", - assistant=en, - )) - - # Financial terminology (EN/FR/DE trilingual) - # Dragon LLM also focused on domain-specific terminology accuracy. - financial_terms = [ - ("balance sheet", "bilan", "Bilanz"), - ("income statement", "compte de résultat", "Gewinn- und Verlustrechnung"), - ("cash flow", "flux de trésorerie", "Cashflow"), - ("equity", "capitaux propres", "Eigenkapital"), - ("dividend yield", "rendement du dividende", "Dividendenrendite"), - ("risk assessment", "évaluation des risques", "Risikobewertung"), - ("regulatory compliance", "conformité réglementaire", "Einhaltung gesetzlicher Vorschriften"), - ("market capitalization", "capitalisation boursière", "Marktkapitalisierung"), - ("asset management", "gestion d'actifs", "Vermögensverwaltung"), - ("due diligence", "diligence raisonnable", "Sorgfaltspflicht"), - ("hedge fund", "fonds spéculatif", "Hedgefonds"), - ("initial public offering", "introduction en bourse", "Börsengang"), - ("credit rating", "notation de crédit", "Kreditbewertung"), - ("derivatives trading", "négoce de produits dérivés", "Derivatehandel"), - ("portfolio diversification", "diversification du portefeuille", "Portfoliodiversifikation"), - ] - - for en, fr, de in financial_terms: - translation_samples.append(make_chat_message( - system="You are a financial terminology expert fluent in English, French, and German.", - user=f"What is the French and German translation of the financial term '{en}'?", - assistant=( - f"The financial term '{en}' translates to:\n" - f"- **French**: {fr}\n" - f"- **German**: {de}\n\n" - f"These terms are commonly used in financial reporting and regulatory " - f"documents across European markets." - ), - )) - - # Pad with resampled examples if we need more to hit the target count - random.seed(config.seed) - base_count = len(translation_samples) - while len(translation_samples) < counts["translation"]: - translation_samples.append(random.choice(translation_samples[:base_count])) - - translation_samples = translation_samples[:counts["translation"]] - print(f" → Total translation: {len(translation_samples)} samples") - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.3 GENERAL DOMAIN DATA (16% of the dataset) - # ═══════════════════════════════════════════════════════════════════════════ - # This is the "forgetting prevention" slice. Dragon LLM used FineWeb-Edu - # (educational web content) and general instruction data to ensure their - # models stayed good at everyday tasks: summarization, explanation, - # creative writing, general Q&A. - # - # We use the Alpaca dataset — a widely-used collection of ~52K general - # instruction/response pairs. It covers topics from science to history - # to cooking, providing broad diversity. - - print(f"\n [3/5] General domain data ({counts['general']} samples)...") - - general_samples = [] - - try: - alpaca = load_dataset("yahma/alpaca-cleaned", split="train") - n_general = min(len(alpaca), counts["general"]) - alpaca_subset = alpaca.shuffle(seed=config.seed).select(range(n_general)) - - for row in alpaca_subset: - instruction = row["instruction"] - inp = row.get("input", "") - output = row["output"] - user_msg = f"{instruction}\n\n{inp}".strip() if inp else instruction - - general_samples.append(make_chat_message( - system="You are a helpful, accurate, and concise assistant.", - user=user_msg, - assistant=output, - )) - print(f" Alpaca-cleaned: {len(general_samples)} samples loaded") - except Exception as e: - print(f" Alpaca-cleaned: failed ({e})") - - general_samples = general_samples[:counts["general"]] - print(f" → Total general: {len(general_samples)} samples") - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.4 RAG DATA (8% of the dataset) - # ═══════════════════════════════════════════════════════════════════════════ - # RAG (Retrieval-Augmented Generation) is a pattern where you provide a - # document to the model and ask it to answer questions grounded in that - # document — rather than relying on its memorized knowledge. - # - # Dragon LLM explicitly stated their models are "RAG-ready", meaning they - # included substantial RAG training data. This teaches the model to: - # 1. Read a provided context carefully - # 2. Answer only from the given information - # 3. Cite or reference the document - # 4. Say "I don't know" when the answer isn't in the context - # - # In financial applications, RAG is critical. Analysts need models that - # can process a 50-page annual report and answer specific questions about - # it — without hallucinating numbers. - # - # We create representative examples using synthetic financial documents - # (annual reports, ECB decisions, KIIDs, risk assessments). - - print(f"\n [4/5] RAG data ({counts['rag']} samples)...") - - rag_samples = [] - - rag_documents = [ - { - "doc": ( - "Annual Report 2024 — TechCorp Inc.\n\n" - "Revenue for fiscal year 2024 reached $12.4 billion, representing a " - "23% year-over-year increase. Operating income was $3.1 billion with " - "an operating margin of 25%. The company repurchased $2.0 billion in " - "common stock during the year. Research and development expenses " - "increased to $1.8 billion, reflecting continued investment in AI and " - "cloud technologies. Total headcount grew to 45,000 employees across " - "30 countries." - ), - "pairs": [ - ( - "What was TechCorp's revenue in fiscal year 2024?", - "According to the Annual Report 2024, TechCorp Inc. reported " - "revenue of $12.4 billion for fiscal year 2024, which represents " - "a 23% year-over-year increase." - ), - ( - "What was the operating margin?", - "Based on the document, TechCorp's operating margin was 25%, " - "with operating income of $3.1 billion on revenue of $12.4 billion." - ), - ( - "How much did TechCorp spend on R&D?", - "The Annual Report states that research and development expenses " - "increased to $1.8 billion, driven by continued investment in AI " - "and cloud technologies." - ), - ], - }, - { - "doc": ( - "ECB Monetary Policy Decision — March 2024\n\n" - "The Governing Council of the European Central Bank decided to keep " - "the three key ECB interest rates unchanged. The main refinancing rate " - "stands at 4.50%, the marginal lending facility rate at 4.75%, and the " - "deposit facility rate at 4.00%. Inflation has continued to decline but " - "remains above the 2% target. The Governing Council emphasized that " - "future policy decisions will be data-dependent and made on a " - "meeting-by-meeting basis. Economic growth in the euro area remained " - "subdued in Q4 2023." - ), - "pairs": [ - ( - "What are the current ECB interest rates?", - "According to the ECB Monetary Policy Decision, the main " - "refinancing rate is 4.50%, the marginal lending facility rate " - "is 4.75%, and the deposit facility rate is 4.00%." - ), - ( - "What is the ECB's inflation target?", - "The document indicates that while inflation has continued to " - "decline, it remains above the ECB's 2% target." - ), - ], - }, - { - "doc": ( - "KIID — European Growth Equity Fund\n\n" - "Objective: The fund aims to achieve long-term capital growth by " - "investing primarily in European equities. Risk Profile: 5 out of 7 " - "(medium-high risk). The fund invests at least 80% of its assets in " - "shares of companies listed on European stock exchanges. The fund may " - "use financial derivative instruments for hedging purposes. Ongoing " - "charges: 1.45% per annum. Entry charge: up to 5%. Exit charge: none. " - "Performance fee: 15% of outperformance above the STOXX Europe 600 index." - ), - "pairs": [ - ( - "What is the risk profile of this fund?", - "According to the KIID document, the European Growth Equity Fund " - "has a risk profile of 5 out of 7, classified as medium-high risk." - ), - ( - "What are the fund's charges?", - "The KIID states the following charges: ongoing charges of 1.45% " - "per annum, an entry charge of up to 5%, no exit charge, and a " - "performance fee of 15% on outperformance above the STOXX Europe " - "600 index." - ), - ( - "What does the fund invest in?", - "Based on the KIID, the fund invests at least 80% of its assets " - "in shares of companies listed on European stock exchanges, with " - "the objective of achieving long-term capital growth. It may also " - "use financial derivative instruments for hedging purposes." - ), - ], - }, - { - "doc": ( - "Risk Assessment Report — Global Credit Portfolio Q3 2024\n\n" - "The portfolio's Value-at-Risk (VaR) at the 99% confidence level " - "stands at EUR 45 million, a decrease of 8% from Q2. Expected " - "Shortfall (CVaR) is EUR 62 million. Credit exposure to " - "investment-grade issuers represents 72% of the portfolio, while " - "high-yield exposure accounts for 28%. Geographic concentration: " - "45% North America, 35% Europe, 15% Asia-Pacific, 5% Emerging " - "Markets. The portfolio's weighted average credit rating is A-. " - "Duration is 4.2 years. Stress test results show maximum portfolio " - "loss of EUR 180 million under a severe recession scenario." - ), - "pairs": [ - ( - "What is the portfolio's VaR?", - "According to the Risk Assessment Report, the portfolio's " - "Value-at-Risk (VaR) at the 99% confidence level is EUR 45 million, " - "representing an 8% decrease from Q2." - ), - ( - "What is the credit quality breakdown?", - "The report indicates that investment-grade issuers represent 72% " - "of the portfolio, while high-yield exposure accounts for 28%. " - "The weighted average credit rating is A-." - ), - ], - }, - ] - - # Convert each document + Q&A pair into a RAG training example - for doc_group in rag_documents: - for question, answer in doc_group["pairs"]: - rag_samples.append(make_chat_message( - system=( - "You are a financial analyst assistant. Answer questions based " - "solely on the provided document. If the information is not in " - "the document, say so." - ), - user=( - f"Based on the following document, answer the question.\n\n" - f"**Document:**\n{doc_group['doc']}\n\n" - f"**Question:** {question}" - ), - assistant=answer, - )) - - # Pad to target count - base_rag = len(rag_samples) - while len(rag_samples) < counts["rag"]: - rag_samples.append(random.choice(rag_samples[:base_rag])) - - rag_samples = rag_samples[:counts["rag"]] - print(f" → Total RAG: {len(rag_samples)} samples") - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.5 MATH & CODE DATA (2% of the dataset) - # ═══════════════════════════════════════════════════════════════════════════ - # The smallest slice, but important. Dragon LLM included math, reasoning, - # and coding data specifically to prevent the base model from losing its - # quantitative skills during fine-tuning. - # - # We focus on financial math (compound interest, bond pricing, portfolio - # returns) and Python code for quantitative finance — tasks that a - # financial LLM should excel at. - - print(f"\n [5/5] Math & code data ({counts['math_code']} samples)...") - - math_code_samples = [] - - # --- Financial math problems with step-by-step solutions --- - math_problems = [ - ( - "Calculate the compound interest on a $10,000 investment at 5% annual rate " - "compounded quarterly for 3 years.", - "Using the compound interest formula: A = P(1 + r/n)^(nt)\n\n" - "Where:\n" - "- P = $10,000 (principal)\n" - "- r = 0.05 (annual rate)\n" - "- n = 4 (quarterly compounding)\n" - "- t = 3 years\n\n" - "A = 10000 × (1 + 0.05/4)^(4×3)\n" - "A = 10000 × (1.0125)^12\n" - "A = 10000 × 1.16075\n" - "A = $11,607.55\n\n" - "Compound interest earned = $11,607.55 - $10,000 = **$1,607.55**" - ), - ( - "A bond has a face value of $1,000, a coupon rate of 6%, and matures in " - "5 years. If the yield to maturity is 4%, what is the bond's price?", - "The bond price is the present value of all future cash flows:\n\n" - "Annual coupon = $1,000 × 6% = $60\n\n" - "Price = Σ [60/(1.04)^t] for t=1 to 5 + 1000/(1.04)^5\n\n" - "= 57.69 + 55.47 + 53.34 + 51.29 + 49.31 + 821.93\n" - "= **$1,089.04**\n\n" - "The bond trades at a premium because the coupon rate (6%) exceeds the " - "yield to maturity (4%)." - ), - ( - "A portfolio has the following weights and returns: Stock A (40%, 12%), " - "Stock B (35%, 8%), Stock C (25%, -3%). Calculate the portfolio return.", - "Portfolio return = Σ (weight × return)\n\n" - "= (0.40 × 12%) + (0.35 × 8%) + (0.25 × -3%)\n" - "= 4.80% + 2.80% + (-0.75%)\n" - "= **6.85%**\n\n" - "The portfolio achieved a positive return of 6.85%, driven primarily by " - "Stock A's strong performance." - ), - ( - "If inflation is 3.2% and the nominal interest rate is 5.5%, what is the " - "real interest rate using the Fisher equation?", - "Using the Fisher equation:\n\n" - "(1 + nominal) = (1 + real) × (1 + inflation)\n\n" - "1.055 = (1 + real) × 1.032\n" - "(1 + real) = 1.055 / 1.032 = 1.02229\n\n" - "Real interest rate = **2.23%**\n\n" - "Note: The simplified approximation (5.5% - 3.2% = 2.3%) gives a close " - "but slightly less accurate result." - ), - ] - - # --- Python code for quantitative finance --- - code_problems = [ - ( - "Write a Python function to calculate the Sharpe ratio of a portfolio.", - '```python\n' - 'def sharpe_ratio(portfolio_returns: list, risk_free_rate: float) -> float:\n' - ' """\n' - ' Calculate the Sharpe Ratio of a portfolio.\n' - ' \n' - ' Args:\n' - ' portfolio_returns: List of periodic returns\n' - ' risk_free_rate: Risk-free rate for the same period\n' - ' \n' - ' Returns:\n' - ' Sharpe ratio as a float\n' - ' """\n' - ' import numpy as np\n' - ' \n' - ' returns = np.array(portfolio_returns)\n' - ' excess_returns = returns - risk_free_rate\n' - ' \n' - ' mean_excess = np.mean(excess_returns)\n' - ' std_excess = np.std(excess_returns, ddof=1)\n' - ' \n' - ' if std_excess == 0:\n' - ' return 0.0\n' - ' \n' - ' return mean_excess / std_excess\n' - '\n' - '# Example:\n' - 'returns = [0.05, 0.02, -0.01, 0.08, 0.03]\n' - 'print(f"Sharpe Ratio: {sharpe_ratio(returns, 0.01):.4f}")\n' - '```' - ), - ( - "Write a Python function to calculate the moving average of a stock price series.", - '```python\n' - 'def moving_average(prices: list, window: int) -> list:\n' - ' """\n' - ' Calculate the simple moving average of a price series.\n' - ' \n' - ' Args:\n' - ' prices: List of stock prices\n' - ' window: Number of periods for the moving average\n' - ' \n' - ' Returns:\n' - ' List of moving average values\n' - ' """\n' - ' if len(prices) < window:\n' - ' return []\n' - ' \n' - ' ma = []\n' - ' for i in range(len(prices) - window + 1):\n' - ' avg = sum(prices[i:i + window]) / window\n' - ' ma.append(round(avg, 2))\n' - ' \n' - ' return ma\n' - '\n' - '# Example:\n' - 'prices = [100, 102, 101, 105, 107, 110, 108]\n' - 'print(f"3-day MA: {moving_average(prices, 3)}")\n' - '```' - ), - ] - - for q, a in math_problems: - math_code_samples.append(make_chat_message( - system="You are a financial mathematics expert. Show your work step by step.", - user=q, - assistant=a, - )) - - for q, a in code_problems: - math_code_samples.append(make_chat_message( - system="You are a Python developer specializing in quantitative finance. Write clean, well-documented code.", - user=q, - assistant=a, - )) - - # Pad to target count - base_mc = len(math_code_samples) - while len(math_code_samples) < counts["math_code"]: - math_code_samples.append(random.choice(math_code_samples[:base_mc])) - - math_code_samples = math_code_samples[:counts["math_code"]] - print(f" → Total math/code: {len(math_code_samples)} samples") - - - # ═══════════════════════════════════════════════════════════════════════════ - # 3.6 COMBINE AND SHUFFLE - # ═══════════════════════════════════════════════════════════════════════════ - # We concatenate all five categories and shuffle. The shuffle is important: - # if the model sees all financial data first, then all translation data, - # it may forget earlier categories by the end of training. Shuffling - # ensures a balanced exposure throughout. - - print(f"\n Combining all data categories...") - - all_samples = ( - finance_samples + - translation_samples + - general_samples + - rag_samples + - math_code_samples - ) - - random.seed(config.seed) - random.shuffle(all_samples) - - dataset = Dataset.from_list(all_samples) - - total = len(all_samples) - print(f"\n Final dataset composition:") - print(f" {'─' * 50}") - print(f" {'Category':<15} {'Count':>6} {'Actual %':>9} {'Target %':>9}") - print(f" {'─' * 50}") - print(f" {'Financial':<15} {len(finance_samples):>6} {len(finance_samples)/total*100:>8.1f}% {config.finance_ratio*100:>8.1f}%") - print(f" {'Translation':<15} {len(translation_samples):>6} {len(translation_samples)/total*100:>8.1f}% {config.translation_ratio*100:>8.1f}%") - print(f" {'General':<15} {len(general_samples):>6} {len(general_samples)/total*100:>8.1f}% {config.general_ratio*100:>8.1f}%") - print(f" {'RAG':<15} {len(rag_samples):>6} {len(rag_samples)/total*100:>8.1f}% {config.rag_ratio*100:>8.1f}%") - print(f" {'Math/Code':<15} {len(math_code_samples):>6} {len(math_code_samples)/total*100:>8.1f}% {config.math_code_ratio*100:>8.1f}%") - print(f" {'─' * 50}") - print(f" {'TOTAL':<15} {total:>6}") - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ STEP 4: FORMATTING DATA FOR TRAINING ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - # - # Our dataset currently contains raw conversation dicts. Before training, - # we need to convert these into the exact text format that Qwen 3 expects. - # - # Each model family has its own chat template — special tokens that mark - # where system prompts, user messages, and assistant responses begin and - # end. Qwen 3's template looks roughly like: - # - # <|im_start|>system - # You are a financial analyst...<|im_end|> - # <|im_start|>user - # What is the P/E ratio?<|im_end|> - # <|im_start|>assistant - # The P/E ratio is...<|im_end|> - # - # The tokenizer's apply_chat_template() method handles this formatting - # automatically. We also set enable_thinking=False because Dragon LLM - # trained without the /think reasoning blocks — though the capability - # is preserved in the final model. - - print("\n\n>>> Step 4: Formatting data for training...") - - def apply_chat_template(example): - """Convert a conversation dict into the model's formatted text.""" - text = tokenizer.apply_chat_template( - example["messages"], - tokenize=False, - add_generation_prompt=False, - enable_thinking=False, - ) - return {"text": text} - - dataset = dataset.map(apply_chat_template, num_proc=1) - - # Let's inspect one formatted example to verify it looks right: - print(f"\n Sample formatted example (first 400 chars):") - print(f" {'─' * 60}") - print(f" {dataset[0]['text'][:400]}") - print(f" {'─' * 60}") - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ STEP 5: TRAINING ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - # - # We use HuggingFace's SFTTrainer (Supervised Fine-Tuning Trainer) from the - # TRL library, running on top of Unsloth's optimized backend. - # - # KEY TRAINING CONCEPTS - # ───────────────────── - # Effective batch size: - # per_device_batch_size (4) × gradient_accumulation_steps (4) = 16 - # This means the model sees 16 examples before each weight update. - # Larger effective batch sizes give more stable gradients but slower - # iteration. - # - # Learning rate schedule: - # We use cosine annealing with a brief warmup. The learning rate starts - # at 0, rises to 2e-4 during the first 5% of steps, then smoothly - # decays back toward 0 following a cosine curve. This is the most - # common schedule for LoRA fine-tuning. - # - # Packing: - # When packing=True, Unsloth concatenates multiple short examples into - # a single sequence (up to max_seq_length). This eliminates padding - # waste and can speed up training by 30-50% for datasets with many - # short examples (like ours). - # - # bf16 vs fp16: - # bfloat16 has the same range as float32 but lower precision. It is - # more numerically stable than fp16 for training. The DGX Spark's - # Blackwell GPU (and all Ampere+ GPUs) supports bf16 natively. - - print("\n\n>>> Step 5: Training the model...") - - from trl import SFTTrainer, SFTConfig - - # --- Detect GPU capabilities --- - if torch.cuda.is_available(): - gpu_name = torch.cuda.get_device_name(0) - gpu_mem_gb = torch.cuda.get_device_properties(0).total_memory / 1e9 - use_bf16 = torch.cuda.is_bf16_supported() - use_fp16 = not use_bf16 - print(f" GPU detected: {gpu_name}") - print(f" Memory: {gpu_mem_gb:.1f} GB") - print(f" Precision: {'bf16' if use_bf16 else 'fp16'}") - else: - use_bf16 = False - use_fp16 = False - print(" WARNING: No GPU detected. Training will be extremely slow.") - - # --- Configure the trainer --- - training_args = SFTConfig( - output_dir=config.output_dir, - - # How long to train - num_train_epochs=config.num_train_epochs, - - # Batch size and accumulation - per_device_train_batch_size=config.per_device_train_batch_size, - gradient_accumulation_steps=config.gradient_accumulation_steps, - - # Learning rate and schedule - learning_rate=config.learning_rate, - warmup_ratio=config.warmup_ratio, - weight_decay=config.weight_decay, - lr_scheduler_type=config.lr_scheduler_type, - max_grad_norm=config.max_grad_norm, - - # Precision - bf16=use_bf16, - fp16=use_fp16, - - # Logging and checkpoints - logging_steps=10, # Print loss every 10 steps - save_strategy="steps", - save_steps=200, - save_total_limit=2, # Keep only last 2 checkpoints - - # Data handling - max_seq_length=config.max_seq_length, - dataset_text_field="text", - packing=True, # Concatenate short examples for efficiency - - # Reproducibility - seed=config.seed, - report_to="none", # Change to "wandb" for Weights & Biases logging - ) - - trainer = SFTTrainer( - model=model, - tokenizer=tokenizer, - train_dataset=dataset, - args=training_args, - ) - - # --- Print a summary before we start --- - eff_batch = config.per_device_train_batch_size * config.gradient_accumulation_steps - est_steps = len(dataset) // eff_batch * config.num_train_epochs - - print(f"\n Training plan:") - print(f" {'─' * 45}") - print(f" {'Epochs:':<30} {config.num_train_epochs}") - print(f" {'Batch size (per device):':<30} {config.per_device_train_batch_size}") - print(f" {'Gradient accumulation:':<30} {config.gradient_accumulation_steps}") - print(f" {'Effective batch size:':<30} {eff_batch}") - print(f" {'Estimated steps:':<30} ~{est_steps}") - print(f" {'Learning rate:':<30} {config.learning_rate}") - print(f" {'LoRA rank:':<30} {config.lora_r}") - print(f" {'Packing:':<30} enabled") - print(f" {'─' * 45}") - print(f"\n Starting training... (this will take 15-60 minutes)\n") - - # --- Train! --- - train_result = trainer.train() - - # --- Report results --- - print(f"\n Training complete!") - print(f" {'─' * 45}") - print(f" {'Total steps:':<30} {train_result.global_step}") - print(f" {'Final training loss:':<30} {train_result.training_loss:.4f}") - print(f" {'Runtime:':<30} {train_result.metrics['train_runtime']:.0f} seconds") - print(f" {'Samples/second:':<30} {train_result.metrics['train_samples_per_second']:.2f}") - print(f" {'─' * 45}") - - # A training loss around 1.0-2.0 is typical for a first fine-tuning run. - # Lower is generally better, but very low loss (<0.5) on limited data - # could indicate overfitting. With only 2000 samples, expect ~1.0-1.5. - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ STEP 6: TESTING THE FINE-TUNED MODEL ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - # - # Now let's see if the training actually worked. We test one prompt from - # each category in our training mix: - # 1. Financial sentiment analysis - # 2. Financial translation - # 3. RAG (document-grounded Q&A) - # 4. Financial math - # 5. General financial knowledge - # - # Note: with only 2000 training samples and a 0.6B model, do not expect - # GPT-4-level answers. The goal here is to verify that the model has - # learned the FORMAT and STYLE of financial responses. Quality improves - # dramatically when you scale up the data and model size. - - print("\n\n>>> Step 6: Testing the fine-tuned model...") - - def generate_response(prompt, system="You are a financial expert.", max_tokens=256): - """ - Run inference on the fine-tuned model. - - This function: - 1. Formats the prompt using Qwen 3's chat template - 2. Tokenizes and sends it to the GPU - 3. Generates a response using sampling (temperature=0.7) - 4. Decodes and returns the text - """ - messages = [ - {"role": "system", "content": system}, - {"role": "user", "content": prompt}, - ] - - inputs = tokenizer.apply_chat_template( - messages, - tokenize=True, - add_generation_prompt=True, # Add the assistant turn start token - return_tensors="pt", - enable_thinking=False, - ).to(model.device) - - with torch.no_grad(): - outputs = model.generate( - input_ids=inputs, - max_new_tokens=max_tokens, - temperature=0.7, - top_p=0.9, - do_sample=True, - ) - - # Decode only the NEW tokens (skip the input prompt) - response = tokenizer.decode( - outputs[0][inputs.shape[-1]:], - skip_special_tokens=True, - ) - return response - - - # --- Test 1: Financial sentiment analysis --- - print(f"\n Test 1: Financial Sentiment Analysis") - print(f" {'─' * 55}") - prompt = ( - "Analyze the sentiment: 'The company's quarterly earnings " - "exceeded analyst expectations, driven by strong growth in " - "cloud services.'" - ) - print(f" Q: {prompt}") - print(f" A: {generate_response(prompt, 'You are a financial sentiment analyst.')}") - - # --- Test 2: Financial translation --- - print(f"\n Test 2: Financial Translation (EN → FR)") - print(f" {'─' * 55}") - prompt = ( - "Translate to French: 'The portfolio's risk-adjusted return " - "outperformed the benchmark index by 150 basis points.'" - ) - print(f" Q: {prompt}") - print(f" A: {generate_response(prompt, 'You are a professional financial translator.')}") - - # --- Test 3: RAG / Document-grounded Q&A --- - print(f"\n Test 3: Document-Grounded Q&A (RAG)") - print(f" {'─' * 55}") - prompt = ( - "Based on the following: 'Q3 revenue was €5.2B, up 18% YoY. " - "EBITDA margin improved to 32%.' What was the EBITDA margin?" - ) - print(f" Q: {prompt}") - print(f" A: {generate_response(prompt, 'You are a financial analyst. Answer based on the provided information only.')}") - - # --- Test 4: Financial math --- - print(f"\n Test 4: Financial Mathematics") - print(f" {'─' * 55}") - prompt = "If a stock is trading at $50 with a P/E ratio of 25, what are the earnings per share?" - print(f" Q: {prompt}") - print(f" A: {generate_response(prompt, 'You are a financial mathematics expert.')}") - - # --- Test 5: General financial knowledge --- - print(f"\n Test 5: Financial Domain Knowledge") - print(f" {'─' * 55}") - prompt = "What is the difference between a UCITS fund and an AIF under European regulations?" - print(f" Q: {prompt}") - print(f" A: {generate_response(prompt, 'You are a European financial regulatory expert.')}") - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ STEP 7: SAVING AND EXPORTING THE MODEL ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - # - # After training, we have a LoRA adapter — a small file (~50-200 MB) that - # modifies the behavior of the base model. We need to save it. - # - # There are several export options: - # - # 1. Save the LoRA adapter only (smallest, requires base model at load) - # 2. Merge adapter into base model and save as 16-bit (standard HF format) - # 3. Export as GGUF for use with llama.cpp / Ollama (local inference) - # 4. Push to HuggingFace Hub (share with the world) - # - # We demonstrate option 1 below. The others are shown as commented code - # you can uncomment when ready. - - print("\n\n>>> Step 7: Saving the model...") - - # --- Option 1: Save LoRA adapter (recommended for development) --- - # This saves only the trained adapter weights, not the full model. - # To use it later, you load the base model + adapter together. - model.save_pretrained(config.output_dir) - tokenizer.save_pretrained(config.output_dir) - print(f" ✓ LoRA adapter saved to: {config.output_dir}/") - print(f" To reload later:") - print(f" >>> model, tokenizer = FastModel.from_pretrained(\"{config.output_dir}\")") - - # --- Option 2: Save merged 16-bit model (full standalone model) --- - # Uncomment to merge the adapter into the base weights and save. - # The result is a standard HuggingFace model that doesn't need Unsloth. - # - # model.save_pretrained_merged( - # f"{config.output_dir}-merged", - # tokenizer, - # save_method="merged_16bit", - # ) - # print(f" ✓ Merged 16-bit model saved to: {config.output_dir}-merged/") - - # --- Option 3: Export as GGUF for llama.cpp / Ollama --- - # GGUF is the format used by llama.cpp and Ollama for local inference. - # q4_k_m is a good balance of quality vs. size. On the DGX Spark, - # the model will run at high speed using the FP4 tensor cores. - # - # model.save_pretrained_gguf( - # f"{config.output_dir}-gguf", - # tokenizer, - # quantization_method="q4_k_m", - # ) - # print(f" ✓ GGUF model saved to: {config.output_dir}-gguf/") - # print(f" Run with: ollama create my-finance-model -f Modelfile") - - # --- Option 4: Push to HuggingFace Hub --- - # Share your model with the community. You'll need a HuggingFace token. - # - # model.push_to_hub("your-username/qwen-open-finance", tokenizer, token="hf_...") - # print(f" ✓ Model pushed to HuggingFace Hub") - - - # ╔════════════════════════════════════════════════════════════════════════════╗ - # ║ ║ - # ║ SUMMARY AND NEXT STEPS ║ - # ║ ║ - # ╚════════════════════════════════════════════════════════════════════════════╝ - - print(f"\n\n{'═' * 60}") - print(f" TUTORIAL COMPLETE") - print(f"{'═' * 60}") - print(f""" - What you built: - ─────────────── - A financial domain LLM fine-tuned from Qwen 3, following the - same methodology Dragon LLM used to create their Open Finance - models. Your model has been trained on a balanced mix of: - - • Financial sentiment, Q&A, and domain knowledge (54%) - • Financial document translation (EN/FR/DE) (20%) - • General instruction data (catastrophe prevention) (16%) - • Document-grounded RAG examples ( 8%) - • Financial math and Python code ( 2%) - - What to do next: - ──────────────── - 1. SCALE THE DATA - Increase total_samples from 2,000 to 50,000+. This single - change will have the biggest impact on quality. - - 2. SCALE THE MODEL - Change model_name to "unsloth/Qwen3-8B" (what Dragon LLM - used). This fits comfortably on the DGX Spark. - - 3. ADD REAL TRANSLATION DATA - Replace our synthetic pairs with OPUS parallel corpora for - authentic financial document translations. - - 4. USE CLASSIFIN FOR DATA FILTERING - Dragon LLM released their ClassiFin classifier on HuggingFace. - Use it to extract high-quality financial content from FineWeb. - - 5. EVALUATE ON BENCHMARKS - Run the model on FPB, FiQA, FinQA, and ConvFinQA to measure - improvement quantitatively. Dragon LLM's evaluation cookbook - is available at: github.com/Dragon-LLM/llm-open-finance-cookbook - - 6. EXPORT FOR PRODUCTION - Uncomment the GGUF export above and serve with Ollama or - llama.cpp for fast local inference on the DGX Spark. - - References: - ─────────── - Paper: arxiv.org/abs/2511.08621 - Models: huggingface.co/collections/DragonLLM/llm-open-finance - ClassiFin: huggingface.co/DragonLLM/ClassiFin - Cookbook: github.com/Dragon-LLM/llm-open-finance-cookbook - Unsloth: unsloth.ai/docs/models/qwen3-how-to-run-and-fine-tune - """) - -if __name__ == "__main__": - main() diff --git a/code/chapter02/quickstart.py b/code/chapter02/quickstart.py index 294a1ea..03bf55d 100644 --- a/code/chapter02/quickstart.py +++ b/code/chapter02/quickstart.py @@ -70,21 +70,25 @@ def step1_prepare_dataset() -> tuple[HFDataset, HFDataset, List[Dict[str, Any]]]: """Step 1: download Dolly 15K and keep 40 train + 5 valid + 3 demo examples. - Same filter and seed as chapter 5's listing_5_2_prepare_dataset.py, just - a smaller slice so the run finishes in minutes. + Same filter and seed as chapter 5's listing_5_2_prepare_dataset.py, just a + smaller slice so the run finishes in minutes. """ print("Step 1: prepare dataset") ds = load_dataset("databricks/databricks-dolly-15k", split="train") rng = random.Random(SEED) + # Length filter: the 20-character floor drops empty or degenerate rows (a + # bare one-word instruction with no real content); the 2000 ceiling keeps + # examples short enough for the 512-token preview. examples: List[Dict[str, Any]] = [] for row in ds: instruction = row.get("instruction", "") context = row.get("context", "") or "" response = row.get("response", "") - total = len(instruction) + len(context) + len(response) - if 20 <= total <= 2000: + total_length = len(instruction) + len(context) + len(response) + if 20 <= total_length <= 2000: examples.append(row) + print(f" {len(examples)} of {len(ds)} examples pass the length filter") rng.shuffle(examples) def to_messages(row: Dict[str, Any]) -> Dict[str, Any]: @@ -244,7 +248,7 @@ def step5_save(trainer: SFTTrainer, tokenizer, samples: List[Dict[str, str]]) -> "dropout": LORA_DROPOUT, "target_modules": TARGET_MODULES, }, - "created_utc": dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z"), + "created_utc": dt.datetime.now(dt.timezone.utc).isoformat().replace("+00:00", "Z"), "samples": samples, } (OUTPUT_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2)) diff --git a/code/chapter03/README.md b/code/chapter03/README.md index 0de03a2..03420ab 100644 --- a/code/chapter03/README.md +++ b/code/chapter03/README.md @@ -13,13 +13,13 @@ This README assumes you have completed the one-time setup from [`code/README.md` **Chapter 3 needs additional packages not in the base install.** Two separate use cases, two separate extras: -- For the data-quality experiment (uses Unsloth on a GPU): +- For the data-quality experiment (transformers + PEFT + TRL, already in the base install): ```bash pip install -e ".[chapter03]" ``` - This pulls `unsloth`, `unsloth_zoo`, `bitsandbytes`, `matplotlib`, and `scikit-learn`. + This pulls `matplotlib` and `scikit-learn` (chart and metrics), `anthropic` (for the synthetic pipeline below), and `bitsandbytes` (only used if you opt into 4-bit on a CUDA GPU; the default is plain bf16, which needs no extra and runs on Apple Silicon and CPU too). - For the synthetic data pipeline (calls a frontier teacher LLM): @@ -36,7 +36,7 @@ The DatasetManifest module (`ch03_datasetmanifest.py`) needs only the Python sta | Use case | GPU | VRAM | Notes | |---|---|---|---| -| Data-quality experiment | NVIDIA, CUDA | ≥ 8 GB | Four short training runs of ~6 minutes each. | +| Data-quality experiment | NVIDIA / Apple Silicon (MPS) / CPU | ~12-16 GB for the default bf16 4B LoRA (or ~6-8 GB with opt-in 4-bit, CUDA only) | Four short LoRA training runs; about 13 minutes total on an A30. | | Synthetic data pipeline | None (API-driven) | n/a | Runs on any machine; cost is API calls only (~$1-3 for a full run on Claude). | | DatasetManifest demos | None | n/a | Pure stdlib + JSON. | @@ -45,7 +45,7 @@ The DatasetManifest module (`ch03_datasetmanifest.py`) needs only the Python sta | File | Contents | |------|----------| | `ch03_data_quality_explore.py` | Main script for §3.1's data-quality experiment. Defines four data-quality conditions and orchestrates train → evaluate → compare. | -| `ch03_data_quality_helpers.py` | Helpers for the experiment: load Financial PhraseBank, inject label noise, compute Cohen's Kappa, train a single condition with LoRA via Unsloth, evaluate against a held-out set, print and chart results. | +| `ch03_data_quality_helpers.py` | Helpers for the experiment: load Financial PhraseBank, inject label noise, compute Cohen's Kappa, train a single condition with LoRA via transformers + PEFT + TRL, evaluate against a held-out set, print and chart results. | | `ch03_synthetic_data_generation.py` | The six-step synthetic data pipeline end to end (load seeds → build prompt → call teacher → quality gate → distribution check → mix and save). | | `ch03_datasetmanifest.py` | DatasetManifest dataclass (SHA-256 hash, source provenance, quality metadata), `diff_manifests`, and `check_retention_status` helpers. | @@ -123,7 +123,7 @@ No GPU, no API key. Walks the manifest creation, diff, and retention-check examp | Asset | Source | Used by | |---|---|---| | Financial PhraseBank | `takala/financial_phrasebank` (HF) | Data-quality experiment (sentence-level sentiment dataset). | -| Qwen2.5-0.5B-Instruct | `unsloth/Qwen2.5-0.5B-Instruct` (HF) | Data-quality experiment (small enough to train four times in under an hour). | +| Qwen3-4B-Instruct-2507 | `Qwen/Qwen3-4B-Instruct-2507` (HF) | Data-quality experiment (the book's spine model; four LoRA runs in about 13 minutes on an A30). | The synthetic data pipeline also pulls seeds from Financial PhraseBank if not already cached. @@ -131,32 +131,26 @@ The synthetic data pipeline also pulls seeds from Financial PhraseBank if not al **Data-quality experiment**: -Four conditions, same 150 training examples each, same hyperparameters. Only the label quality differs. Representative numbers from a single A30 run with seed 42: +Four conditions, same 150 training examples each, same hyperparameters. Only the label quality differs. Representative numbers from a single A30 bf16 run (seed 42): -| Condition | Description | Accuracy | -|---|---|---| -| A | Clean labels, balanced classes | ~0.78 | -| B | Mild noise (5%), balanced | ~0.74 | -| C | Heavy noise (20%), balanced | ~0.62 | -| D | Clean labels, imbalanced (60/30/10) | ~0.71 | +| Condition | Description | Accuracy | macro-F1 | +|---|---|---|---| +| A | AllAgree (clean: all annotators agreed) | 0.96 | 0.95 | +| B | 75Agree (good: typical production quality) | 0.98 | 0.97 | +| C | 50Agree (noisy: bare-majority agreement) | 0.96 | 0.95 | +| D | Corrupted (clean sentences, 20% of labels flipped) | 0.84 | 0.82 | -The chapter's argument: condition C (heavy noise) outperforms naive intuition (you might expect it to collapse), and condition D (imbalanced) underperforms more than label noise does at the same volume. +The chapter's argument: the systematically corrupted condition D drops sharply (0.84) while natural annotator disagreement (A, B, C) stays high (0.96 to 0.98), because a model tolerates fuzzy labels but cannot learn from contradictory ones. Numbers vary run to run; the relative shape is the point. **Synthetic data pipeline**: produces 100-200 verified synthetic examples per category at the end of a single run, with a manifest JSON capturing source, hash, and quality metadata. ## Troubleshooting -**"No module named 'unsloth'"** → Run `pip install -e ".[chapter03]"` from `code/`. - **"ANTHROPIC_API_KEY not found"** → The synthetic data pipeline calls Claude; export the key first (`export ANTHROPIC_API_KEY=sk-ant-...`). -**"sklearn not installed"** → Included in the `chapter03` extra; install with `pip install -e ".[chapter03]"`. - -**Matplotlib import error** → Same answer; the chapter extra includes matplotlib. The script gracefully skips the chart if matplotlib is missing. - -**`fatal error: Python.h: No such file or directory`** → Triton's runtime compiler needs the Python development headers. Install `python3.X-dev` (`sudo apt install python3.12-dev` on Ubuntu) and export `CPATH=/usr/include/python3.12` before running the script. +**"sklearn not installed" / Matplotlib import error** → Both are in the `chapter03` extra; install with `pip install -e ".[chapter03]"`. The script gracefully skips the chart if matplotlib is missing. -**`NameError: VARIANT_KWARG_KEYS is not defined`** → Unsloth's compile-cache generator does not yet support PEFT 0.18+. The book pins `peft<0.18` in `pyproject.toml`; if your environment has a newer PEFT, run `pip install "peft<0.18"` and delete `code/unsloth_compiled_cache/` before retrying. +**Out of memory** → The default loads the 4B model in bf16, which wants ~12-16 GB. On a smaller CUDA GPU, set `LOAD_IN_4BIT=True` in `ch03_data_quality_helpers.py` to use 4-bit (needs `bitsandbytes`, CUDA only). On Apple Silicon, use a 16 GB+ machine. **`Can't pickle ` during dataset map** → Triggered by `safetensors>=0.6`. The book pins `safetensors<0.6`; reinstall with `pip install "safetensors<0.6"` if a newer version has crept in. diff --git a/code/chapter03/ch03_data_quality_explore.py b/code/chapter03/ch03_data_quality_explore.py index c0b67a7..40130f2 100644 --- a/code/chapter03/ch03_data_quality_explore.py +++ b/code/chapter03/ch03_data_quality_explore.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 """ -ch03_3_1_data_quality_impact.py -──────────────────────────────── +ch03_data_quality_explore.py +──────────────────────────── Chapter 3, Section 3.1 — Why Data Quality Is the #1 Factor Demonstrates how data quality alone — with volume held constant — determines model accuracy. Four conditions, same 150 training examples each, same model, same hyperparameters. Only the label quality differs. -Requires: ch03_3_1_quality_experiment_helpers.py (in the same directory) +Requires: ch03_data_quality_helpers.py (in the same directory) Hardware: NVIDIA GPU with ≥ 8 GB VRAM Time : ~25–35 minutes (4 training runs × ~6 minutes) Install: - pip install unsloth unsloth_zoo datasets transformers trl + pip install datasets transformers trl peft bitsandbytes accelerate pip install huggingface_hub scikit-learn matplotlib """ @@ -22,13 +22,7 @@ import warnings from collections import Counter -# Suppress known bitsandbytes FutureWarning spam from PyTorch internals. -warnings.filterwarnings( - "ignore", - message=r".*_check_is_size will be removed in a future PyTorch release.*", - category=FutureWarning, - module=r"bitsandbytes\._ops", -) +warnings.filterwarnings("ignore", category=FutureWarning) from chapter03.ch03_data_quality_helpers import ( load_phrasebank_split, diff --git a/code/chapter03/ch03_data_quality_helpers.py b/code/chapter03/ch03_data_quality_helpers.py index af846ee..4838e13 100644 --- a/code/chapter03/ch03_data_quality_helpers.py +++ b/code/chapter03/ch03_data_quality_helpers.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 """ -ch03_3_1_quality_experiment_helpers.py -─────────────────────────────────────── +ch03_data_quality_helpers.py +───────────────────────────── Infrastructure for the data quality impact experiment. This file contains all functions that are NOT about the data quality story: - Loading Financial PhraseBank from HuggingFace - Injecting synthetic label noise - Computing Cohen's Kappa - - Training one model condition with LoRA via Unsloth + - Training one model condition with LoRA via TRL + PEFT - Evaluating a checkpoint on a held-out set - Printing the results table and saving the chart -The main file (ch03_3_1_data_quality_impact.py) calls these functions. +The main file (ch03_data_quality_explore.py) calls these functions. Keeping them here means the main file stays focused on the experiment design. Install: - pip install unsloth unsloth_zoo datasets transformers trl + pip install datasets transformers trl peft bitsandbytes accelerate pip install huggingface_hub scikit-learn matplotlib """ @@ -24,9 +24,6 @@ import json import random import zipfile -import builtins -from contextlib import contextmanager -from collections import Counter from huggingface_hub import hf_hub_download from datasets import Dataset @@ -35,13 +32,13 @@ # ── Shared constants ────────────────────────────────────────────────────────── # These mirror the values in the main file. # Both files import from here so they can never drift out of sync. -MODEL_NAME = "unsloth/Qwen2.5-0.5B-Instruct" +MODEL_NAME = "Qwen/Qwen3-4B-Instruct-2507" MAX_SEQ_LENGTH = 512 -LOAD_IN_4BIT = True +LOAD_IN_4BIT = False # plain bf16 LoRA: runs on CUDA, Apple Silicon (MPS), and CPU LORA_R = 8 LORA_ALPHA = 8 -TARGET_MODULES = ["q_proj","k_proj","v_proj","o_proj", - "gate_proj","up_proj","down_proj"] +TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj", + "gate_proj", "up_proj", "down_proj"] TRAIN_EPOCHS = 1 BATCH_SIZE = 4 GRAD_ACCUM = 2 @@ -54,29 +51,6 @@ "Respond with exactly one word: positive, negative, or neutral." ) -_UNSLOTH_SUPPRESSED_PRINT_SNIPPETS = ( - "Unsloth: Your Flash Attention 2 installation seems to be broken.", - "Using Xformers instead. No performance changes will be seen.", -) - - -@contextmanager -def _suppress_unsloth_flashattn_notice(): - """Temporarily suppress noisy Unsloth FlashAttention fallback print lines.""" - original_print = builtins.print - - def filtered_print(*args, **kwargs): - message = " ".join(str(a) for a in args) - if any(snippet in message for snippet in _UNSLOTH_SUPPRESSED_PRINT_SNIPPETS): - return - return original_print(*args, **kwargs) - - builtins.print = filtered_print - try: - yield - finally: - builtins.print = original_print - # ══════════════════════════════════════════════════════════════════════════════ # DATA LOADING @@ -235,13 +209,26 @@ def make_hf_dataset(examples: list[dict]) -> Dataset: # TRAINING # ══════════════════════════════════════════════════════════════════════════════ +def _make_bnb_config(): + """Build a 4-bit BitsAndBytes quantization config (nf4 + double quant).""" + import torch + from transformers import BitsAndBytesConfig + + return BitsAndBytesConfig( + load_in_4bit=LOAD_IN_4BIT, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=torch.bfloat16, + bnb_4bit_use_double_quant=True, + ) + + def train_condition( condition_key: str, train_examples: list[dict], output_base: str, ) -> str: """ - Fine-tune Qwen2.5-0.5B-Instruct on one experimental condition using LoRA. + Fine-tune Qwen3-4B-Instruct on one experimental condition using LoRA. All hyperparameters are fixed (imported from the constants above) so the only variable between calls is the training data. This isolation is what @@ -255,32 +242,61 @@ def train_condition( Returns: Path to the saved checkpoint directory """ - with _suppress_unsloth_flashattn_notice(): - from unsloth import FastModel - from unsloth.chat_templates import get_chat_template + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer + from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training from trl import SFTTrainer, SFTConfig output_dir = os.path.join(output_base, condition_key) os.makedirs(output_dir, exist_ok=True) - # Fresh model load for each condition — no weight sharing between runs - with _suppress_unsloth_flashattn_notice(): - model, tokenizer = FastModel.from_pretrained( - model_name=MODEL_NAME, - max_seq_length=MAX_SEQ_LENGTH, - load_in_4bit=LOAD_IN_4BIT, + # Fresh model load for each condition — no weight sharing between runs. + # Default is plain bf16 (LOAD_IN_4BIT=False) so the experiment runs on CUDA, + # Apple Silicon (MPS), and CPU. 4-bit is opt-in and CUDA-only (bitsandbytes). + bnb_config = _make_bnb_config() if LOAD_IN_4BIT else None + # device_map="auto" is for CUDA (multi-GPU / offload). On Apple Silicon + # (MPS) it mis-dispatches under gradient checkpointing ("expected device + # meta but got mps"), so place the whole model on one device instead. + device_map = "auto" if torch.cuda.is_available() else None + model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + quantization_config=bnb_config, + device_map=device_map, + dtype=torch.bfloat16, + ) + if device_map is None: + model = model.to("mps" if torch.backends.mps.is_available() else "cpu") + tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + # Sync both model.config and model.generation_config — transformers checks both + model.config.pad_token_id = tokenizer.pad_token_id + model.config.bos_token_id = tokenizer.bos_token_id + model.generation_config.pad_token_id = tokenizer.pad_token_id + model.generation_config.bos_token_id = tokenizer.bos_token_id + + # For 4-bit, this is what makes gradients flow into the LoRA adapter (without + # it, grad_norm stays 0 and nothing learns). Harmless to skip for bf16, where + # enable_input_require_grads() below is enough for gradient checkpointing. + if LOAD_IN_4BIT: + model = prepare_model_for_kbit_training( + model, use_gradient_checkpointing=True ) - tokenizer = get_chat_template(tokenizer, chat_template="qwen-2.5") + + # Required so gradient checkpointing works with frozen base layers + model.enable_input_require_grads() # LoRA adapter — same rank/alpha for every condition - model = FastModel.get_peft_model( - model, - r=LORA_R, lora_alpha=LORA_ALPHA, + lora_config = LoraConfig( + r=LORA_R, + lora_alpha=LORA_ALPHA, target_modules=TARGET_MODULES, lora_dropout=0.0, - use_gradient_checkpointing="unsloth", - random_state=42, + task_type=TaskType.CAUSAL_LM, + bias="none", ) + model = get_peft_model(model, lora_config) + model.print_trainable_parameters() # Apply chat template to convert role/content dicts → token strings def format_fn(batch): @@ -295,7 +311,7 @@ def format_fn(batch): trainer = SFTTrainer( model=model, - tokenizer=tokenizer, + processing_class=tokenizer, train_dataset=dataset, args=SFTConfig( output_dir=output_dir, @@ -311,17 +327,19 @@ def format_fn(batch): dataset_text_field="text", report_to="none", logging_steps=25, + gradient_checkpointing=True, + gradient_checkpointing_kwargs={"use_reentrant": False}, ), ) stats = trainer.train() + # Save the LoRA adapter and tokenizer model.save_pretrained(output_dir) tokenizer.save_pretrained(output_dir) print(f" training loss: {stats.training_loss:.4f} → saved to {output_dir}") # Free GPU memory before the next condition is loaded del model, trainer - import torch torch.cuda.empty_cache() return output_dir @@ -349,22 +367,35 @@ def evaluate_condition( Returns: dict with accuracy, macro_f1, per_class_f1, and predictions list """ - with _suppress_unsloth_flashattn_notice(): - from unsloth import FastModel - from unsloth.chat_templates import get_chat_template + import torch + from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig + from peft import PeftModel from sklearn.metrics import accuracy_score, f1_score - from transformers import GenerationConfig - import torch + bnb_config = _make_bnb_config() if LOAD_IN_4BIT else None - with _suppress_unsloth_flashattn_notice(): - model, tokenizer = FastModel.from_pretrained( - model_name=checkpoint_path, - max_seq_length=MAX_SEQ_LENGTH, - load_in_4bit=LOAD_IN_4BIT, - ) - tokenizer = get_chat_template(tokenizer, chat_template="qwen-2.5") - FastModel.for_inference(model) + # Load the frozen base model, then layer the saved LoRA adapter on top. + # device_map="auto" only on CUDA; single-device on MPS/CPU (see train_condition). + device_map = "auto" if torch.cuda.is_available() else None + base_model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, + quantization_config=bnb_config, + device_map=device_map, + dtype=torch.bfloat16, + ) + if device_map is None: + base_model = base_model.to("mps" if torch.backends.mps.is_available() else "cpu") + model = PeftModel.from_pretrained(base_model, checkpoint_path) + model.eval() + + tokenizer = AutoTokenizer.from_pretrained(checkpoint_path) + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + # Sync both model.config and model.generation_config — transformers checks both + model.config.pad_token_id = tokenizer.pad_token_id + model.config.bos_token_id = tokenizer.bos_token_id + model.generation_config.pad_token_id = tokenizer.pad_token_id + model.generation_config.bos_token_id = tokenizer.bos_token_id # Avoid repeated warning: do not keep both max_length and max_new_tokens. gen_cfg = GenerationConfig.from_model_config(model.config) @@ -388,11 +419,12 @@ def evaluate_condition( tokenize=False, add_generation_prompt=True, # appends the assistant turn marker ) - inputs = tokenizer(prompt, return_tensors="pt").to("cuda") - outputs = model.generate( - **inputs, - generation_config=gen_cfg, - ) + inputs = tokenizer(prompt, return_tensors="pt").to(model.device) + with torch.no_grad(): + outputs = model.generate( + **inputs, + generation_config=gen_cfg, + ) # Decode only the tokens the model generated (not the prompt) response = tokenizer.decode( outputs[0][inputs["input_ids"].shape[1]:], @@ -403,7 +435,7 @@ def evaluate_condition( first_word = (response.split() or ["neutral"])[0] predictions.append(first_word if first_word in LABEL_SET else "neutral") - del model + del model, base_model torch.cuda.empty_cache() # Per-class F1 @@ -508,9 +540,9 @@ def save_accuracy_chart(results: dict[str, dict], output_dir: str) -> str | None return None ORDER = ["A_clean", "B_good", "C_noisy", "D_corrupted"] - labels = [results[k]["label"] for k in ORDER] - accs = [results[k]["accuracy"]*100 for k in ORDER] - kappas = [results[k]["kappa"] for k in ORDER] + labels = [results[k]["label"] for k in ORDER] + accs = [results[k]["accuracy"]*100 for k in ORDER] + kappas = [results[k]["kappa"] for k in ORDER] hatches = ["", "//", "xx", ".."] fig, ax = plt.subplots(figsize=(9, 5)) diff --git a/code/chapter04/rag_minimal.py b/code/chapter04/rag_minimal.py index f366b87..a901ecc 100644 --- a/code/chapter04/rag_minimal.py +++ b/code/chapter04/rag_minimal.py @@ -35,6 +35,7 @@ from __future__ import annotations import argparse +import hashlib import re from dataclasses import dataclass, field from typing import Callable, Dict, List, Optional, Sequence @@ -53,10 +54,21 @@ def _tokenize(text: str) -> List[str]: return re.sub(r"[^a-z0-9\s]", " ", text.lower()).split() +def _token_bucket(token: str, dim: int) -> int: + """Map a token to a bucket with a stable, process-independent hash. + + Python's built-in ``hash`` is salted per process (PYTHONHASHSEED), so using + it here would make embeddings, and therefore retrieval ranking, change from + run to run. blake2b gives the same bucket every time. + """ + digest = hashlib.blake2b(token.encode("utf-8"), digest_size=8).digest() + return int.from_bytes(digest, "big") % dim + + def hash_embed(texts: Sequence[str], dim: int = 128) -> np.ndarray: """Token-hash fallback embedder (no models required). - Maps tokens to indices via Python's ``hash`` and accumulates a sparse + Maps tokens to indices via a stable hash and accumulates a sparse bag-of-words vector that is L2-normalised. Accuracy is well below a real embedder, but the implementation is deterministic and fast and is enough to exercise the pipeline in tests. @@ -64,7 +76,7 @@ def hash_embed(texts: Sequence[str], dim: int = 128) -> np.ndarray: vectors = np.zeros((len(texts), dim), dtype=np.float32) for i, text in enumerate(texts): for tok in _tokenize(text): - vectors[i, abs(hash(tok)) % dim] += 1.0 + vectors[i, _token_bucket(tok, dim)] += 1.0 norm = float(np.linalg.norm(vectors[i])) if norm > 0: vectors[i] /= norm diff --git a/code/chapter05/README.md b/code/chapter05/README.md index 2843b9f..b1f4193 100644 --- a/code/chapter05/README.md +++ b/code/chapter05/README.md @@ -66,7 +66,7 @@ We use **`databricks/databricks-dolly-15k`** because: ### One-Time Setup (Fresh Machine) **First-time setup:** If you haven't set up the book environment yet, follow the detailed instructions in **`code/README.md`** (one directory up). This includes: -- Checking Python version (**3.10+ required**) +- Checking Python version (**3.12+ required**) - Installing system prerequisites (Ubuntu/Debian: `python3-venv`) - Creating virtual environment - Installing PyTorch (CPU or CUDA) @@ -82,6 +82,8 @@ pip install -e ".[qlora]" QLoRA is optional. If you do not plan to run Step 5, you can skip this extra. +> **On a Mac?** QLoRA (Step 5) does not run on Apple Silicon: `bitsandbytes` 4-bit kernels are CUDA/ROCm-only, with no Metal/MPS build. Removing `bitsandbytes` would not make QLoRA run on a Mac, it would just remove the 4-bit path that makes it QLoRA. Use the LoRA branch (Steps 1-4), which needs no `bitsandbytes` and trains on MPS. See [ACCELERATORS.md](../../ACCELERATORS.md#why-qlora-needs-an-nvidia-or-amd-gpu) for the full explanation. + ### Verify Your Setup (Recommended) Before investing time in full training runs, validate that everything is installed correctly: diff --git a/code/chapter05/scripts/listing_5_2_prepare_dataset.py b/code/chapter05/scripts/listing_5_2_prepare_dataset.py index de166bd..77f7e16 100644 --- a/code/chapter05/scripts/listing_5_2_prepare_dataset.py +++ b/code/chapter05/scripts/listing_5_2_prepare_dataset.py @@ -184,7 +184,7 @@ def main() -> None: manifest = { "dataset": "databricks/databricks-dolly-15k", "split": "train", - "created_utc": dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z"), + "created_utc": dt.datetime.now(dt.timezone.utc).isoformat().replace("+00:00", "Z"), "seed": args.seed, "filters": { "min_length": args.min_length, diff --git a/code/pyproject.toml b/code/pyproject.toml index e6c4dc9..67d8869 100644 --- a/code/pyproject.toml +++ b/code/pyproject.toml @@ -7,14 +7,14 @@ name = "model-adaptation-book" version = "0.1.0" description = "Runnable code for the book Practical Model Adaptation Techniques for Large Language Models." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.12" dependencies = [ "accelerate>=0.30.0", "datasets>=2.19.0", "evaluate>=0.4.2", "huggingface_hub>=0.23.0", - # PEFT 0.18+ moves VARIANT_KWARG_KEYS in a way unsloth's compile cache - # cannot resolve. Cap at <0.18 until unsloth catches up. + # Capped to pair with the transformers<5.0 cap below: peft<0.18 imports + # HybridCache, which transformers 5.x removed. "peft>=0.12.0,<0.18", "rich>=13.7.0", # safetensors 0.6+ dropped builtins.safe_open, which breaks datasets.map @@ -22,7 +22,9 @@ dependencies = [ # datasets stops fingerprinting through it. "safetensors>=0.4.3,<0.6", "sentence-transformers>=3.0.0", - "transformers>=4.47.0", + # Capped below 5.0: transformers 5.x removed HybridCache, which peft<0.18 + # imports at load time, so a fresh install on 5.x breaks `import peft`. + "transformers>=4.47.0,<5.0", "trl>=0.9.0", "python-dotenv>=1.0.1", ] @@ -31,22 +33,12 @@ dependencies = [ # QLoRA / quantization support. Keep optional for macOS/CPU-only readers. qlora = ["bitsandbytes>=0.43.0"] -# Chapter 2 — Unsloth-based open-finance fine-tuning quick-start. -# Pulls Unsloth's fast training kernels plus bitsandbytes for 4-bit support. -# Linux/CUDA only; do not install on macOS or CPU-only environments. -chapter02 = [ - "unsloth>=2024.0", - "unsloth_zoo>=2024.0", - "bitsandbytes>=0.43.0", -] - # Chapter 3 — data-quality experiment and synthetic data pipeline. -# Includes Unsloth (for the §3.1 experiment), the Anthropic SDK (for the -# §3.7 synthetic data pipeline), and the plotting / metrics packages -# the experiment script uses. +# The §3.1 experiment uses transformers + PEFT + TRL (core deps) in bf16 by +# default; bitsandbytes enables the optional 4-bit path. The §3.7 synthetic +# pipeline uses the Anthropic SDK; the experiment plots/scores with matplotlib +# + scikit-learn. chapter03 = [ - "unsloth>=2024.0", - "unsloth_zoo>=2024.0", "bitsandbytes>=0.43.0", "anthropic>=0.30.0", "matplotlib>=3.8", From 9aa0a4d045b9ad66c84401f8e92a72f34c713efe Mon Sep 17 00:00:00 2001 From: Amit Bahree Date: Sun, 7 Jun 2026 12:48:03 -0700 Subject: [PATCH 2/2] Chapter 5 updates based on reviews --- README.md | 4 +- code/README.md | 2 +- code/chapter05/README.md | 3 + .../example_data_prep_outcome_types.md | 64 ++++ code/it_support_qa/.gitignore | 3 + code/it_support_qa/LICENSE | 16 + code/it_support_qa/README.md | 87 +++++ code/it_support_qa/capture_outputs.py | 37 +++ code/it_support_qa/data/expanded/eval.jsonl | 6 + code/it_support_qa/data/expanded/train.jsonl | 166 ++++++++++ code/it_support_qa/data/it_qa_train.jsonl | 24 ++ code/it_support_qa/data/quality_pairs.jsonl | 5 + code/it_support_qa/expand_seed.py | 168 ++++++++++ code/it_support_qa/lora_domain_demo.py | 152 +++++++++ code/it_support_qa/make_it_qa.py | 229 +++++++++++++ code/it_support_qa/plot_domain.py | 54 ++++ code/it_support_qa/results/contrast.png | Bin 0 -> 61404 bytes code/it_support_qa/results/contrast.svg | 301 ++++++++++++++++++ code/it_support_qa/results/results.json | 20 ++ .../it_support_qa/results/sample_outputs.json | 16 + code/it_support_qa/topup_hardware.py | 62 ++++ 21 files changed, 1416 insertions(+), 3 deletions(-) create mode 100644 code/chapter05/examples/example_data_prep_outcome_types.md create mode 100644 code/it_support_qa/.gitignore create mode 100644 code/it_support_qa/LICENSE create mode 100644 code/it_support_qa/README.md create mode 100644 code/it_support_qa/capture_outputs.py create mode 100644 code/it_support_qa/data/expanded/eval.jsonl create mode 100644 code/it_support_qa/data/expanded/train.jsonl create mode 100644 code/it_support_qa/data/it_qa_train.jsonl create mode 100644 code/it_support_qa/data/quality_pairs.jsonl create mode 100644 code/it_support_qa/expand_seed.py create mode 100644 code/it_support_qa/lora_domain_demo.py create mode 100644 code/it_support_qa/make_it_qa.py create mode 100644 code/it_support_qa/plot_domain.py create mode 100644 code/it_support_qa/results/contrast.png create mode 100644 code/it_support_qa/results/contrast.svg create mode 100644 code/it_support_qa/results/results.json create mode 100644 code/it_support_qa/results/sample_outputs.json create mode 100644 code/it_support_qa/topup_hardware.py diff --git a/README.md b/README.md index d7184ac..3ae165a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Fine-Tuning and Customizing LLMs for Enterprises +# LLM Customization and Fine-Tuning: Adaptation, Distillation, and Alignment -Welcome to the code repository for **Fine-Tuning and Customizing LLMs for Enterprises** (working title). +Welcome to the code repository for **LLM Customization and Fine-Tuning: Adaptation, Distillation, and Alignment** (Manning Publications). This repository contains all the runnable code, data, and examples from the book, organized by chapter. diff --git a/code/README.md b/code/README.md index 340d70e..430bad1 100644 --- a/code/README.md +++ b/code/README.md @@ -1,6 +1,6 @@ # Book code workspace -Welcome to the code repo for the **Model Adaptation** book. This folder contains the runnable code, organized by chapter. +Welcome to the code repo for **LLM Customization and Fine-Tuning: Adaptation, Distillation, and Alignment** (Manning Publications). This folder contains the runnable code, organized by chapter. **Repository**: diff --git a/code/chapter05/README.md b/code/chapter05/README.md index b1f4193..e9e719f 100644 --- a/code/chapter05/README.md +++ b/code/chapter05/README.md @@ -182,6 +182,8 @@ Wrote Dolly 15K subset to: chapter05/data/dolly_subset Dolly 15K has 8 task categories (`open_qa`, `general_qa`, `closed_qa`, `summarization`, `brainstorming`, `classification`, `information_extraction`, `creative_writing`); with `--seed 42 --train 400` the breakdown above is what you will see. +**Outcome types in your own data:** Dolly contains no refusals or tone-tagged examples, which are response types you typically add for an internal assistant. For worked `messages`-format rows showing a refusal, a clarification, and a tone tag (plus a note on inter-annotator agreement for Q&A), see [examples/example_data_prep_outcome_types.md](examples/example_data_prep_outcome_types.md). + ### Step 2: Train LoRA Adapter Train a LoRA adapter using TRL's SFTTrainer: @@ -492,6 +494,7 @@ python chapter05/scripts/publish_adapter.py ^ ## See Also +- [Contoso domain-adaptation example, where an adapter beats prompting (base vs. format-prompt vs. LoRA, with sample outputs)](../it_support_qa/README.md) — the section 5.1.8 / figure 5.5 example, full dataset and reproducible run - [Base vs LoRA vs QLoRA inference output (same prompt)](examples/example_inference_base_vs_adapter.md) - [QLoRA training log and interpretation](examples/example_qlora_training_output.md) - [LoRA vs QLoRA evaluation run](examples/example_qlora_evaluation_output.md) diff --git a/code/chapter05/examples/example_data_prep_outcome_types.md b/code/chapter05/examples/example_data_prep_outcome_types.md new file mode 100644 index 0000000..043bf13 --- /dev/null +++ b/code/chapter05/examples/example_data_prep_outcome_types.md @@ -0,0 +1,64 @@ +# Data-prep outcome types (worked examples) + +These illustrate the response types discussed in the chapter's "Data quality +iterations" section, using the Contoso IT-support assistant. Each is a single +training row in the same `messages` format produced by +`scripts/listing_5_2_prepare_dataset.py` (see `dolly_to_messages`). + +> **These rows are illustrative.** The Dolly 15K subset used in this chapter +> contains no refusals and no tone tags, so these are examples of what you would +> *add* for an internal assistant. They are not part of the committed +> `data/dolly_subset/`, and adding them would change the training mix and the +> eval numbers, so the pipeline is left as-is. + +## Refusal (decline, then redirect) + +A response type Dolly never contains. Teaching it explicitly is one fix for the +safety-refusal regression discussed in the Results section. + +```json +{"messages": [ + {"role": "system", "content": "You are Contoso's internal IT support assistant."}, + {"role": "user", "content": "What is the admin password for the finance file share?"}, + {"role": "assistant", "content": "I can't share admin passwords. To get access, open the IT portal, go to Access > File Shares, and submit a request for your manager to approve."} +]} +``` + +## Clarification (ask instead of guessing) + +When the request is underspecified, the target behaviour is to ask for the +missing detail rather than hallucinate one. + +```json +{"messages": [ + {"role": "system", "content": "You are Contoso's internal IT support assistant."}, + {"role": "user", "content": "My laptop will not connect."}, + {"role": "assistant", "content": "Let us narrow it down: are you trying to reach Wi-Fi, the VPN, or a wired connection, and is there an error on screen? Meanwhile, confirm Wi-Fi is on and you are in range of a Contoso access point."} +]} +``` + +## Tone tag (marker in the system prompt, repeated at inference) + +```json +{"messages": [ + {"role": "system", "content": "[Tone: Friendly] You are Contoso's internal IT support assistant."}, + {"role": "user", "content": "How do I reset my password?"}, + {"role": "assistant", "content": "Quick one! Head to portal.contoso.com/reset, sign in with your badge ID, and follow the prompts. You will be back in within a couple of minutes."} +]} +``` + +The `[Tone: Friendly]` marker is part of the system prompt during training; +prepend the same marker at inference to select that voice. Keep one marker +vocabulary across the dataset, or train a separate adapter per tone. + +## Inter-annotator agreement on Q&A + +Agreement is harder to pin down on Q&A than on classification. Ask two support +engineers to write the gold answer for "How do I install the latest Node LTS on +my dev machine?" and you may get two reasonable but different answers: one points +to Contoso's internal `dev-setup` tool and its approved steps, the other gives a +generic `nvm` command. A three-label sentiment task rarely has this ambiguity; an +open-ended Q&A answer routinely does. Because a model's ceiling is roughly the +agreement level of its labels, write an answer-key guideline (preferred tools, +required steps, house format) before annotating, so reviewers converge on one +shape rather than several defensible ones. diff --git a/code/it_support_qa/.gitignore b/code/it_support_qa/.gitignore new file mode 100644 index 0000000..1f08172 --- /dev/null +++ b/code/it_support_qa/.gitignore @@ -0,0 +1,3 @@ +runs/ +__pycache__/ +*.pyc diff --git a/code/it_support_qa/LICENSE b/code/it_support_qa/LICENSE new file mode 100644 index 0000000..0160eea --- /dev/null +++ b/code/it_support_qa/LICENSE @@ -0,0 +1,16 @@ +CC0 1.0 Universal (Public Domain Dedication) + +The author(s) have dedicated this dataset (the hand-authored Contoso IT-support +Q&A in this folder: it_qa_train.jsonl, quality_pairs.jsonl, and the curated +content in make_it_qa.py) to the public domain by waiving all of their rights to +the work worldwide under copyright law, including all related and neighboring +rights, to the extent allowed by law. + +You can copy, modify, distribute, and use the work, even for commercial +purposes, all without asking permission. + +The work is provided "as is" without warranty of any kind. + +Full legal text: https://creativecommons.org/publicdomain/zero/1.0/legalcode + +Note: "Contoso" is a fictional company and all tool names herein are invented. diff --git a/code/it_support_qa/README.md b/code/it_support_qa/README.md new file mode 100644 index 0000000..dac2551 --- /dev/null +++ b/code/it_support_qa/README.md @@ -0,0 +1,87 @@ +# Contoso IT-support Q&A (starter domain dataset) + +A small, **hand-authored, CC0-licensed** IT-support Q&A set built to fill two gaps the tech editor (Andrew) flagged across chapters 5 to 8. It is deliberately **ours and self-contained**, so it does not depend on the Chapter 3 IT rewrite and carries no third-party licensing. + +> Used in the book: the Contoso base-vs-adapter contrast in **chapter 5** (section 5.1.8, "Where an adapter beats prompting," figure 5.5) and the Q&A data-prep examples draw on this set, and the same assistant is carried forward in chapters 6, 8, and 9. It is still a compact starter (see the note on size at the end): enough to illustrate the contrast, not to ship. + +## Why this exists + +1. **Data quality for Q&A** (`data/quality_pairs.jsonl`). The book defines "high-quality data" only for classification (consistent labels). For Q&A it only asserts quality without showing it. These good-vs-bad pairs make Q&A quality concrete: each `bad` variant carries a `flaw` label (incomplete, wrong_format, hallucinated_detail, off_tone, inconsistent_terms, dangerous_wrong, generic_no_domain). +2. **A domain-adaptation example where LoRA beats prompting** (`data/it_qa_train.jsonl`). Every worked example so far uses generic Dolly Q&A, where a base model plus a prompt is already fine. This set is domain-specific in a way a prompt cannot fake. + +## The domain hook (why prompting cannot match it) + +Answers use **Contoso-internal tool names** and a **fixed micro-format** the base model has never seen: + +- Internal terms: GlobalConnect VPN, AccessHub, Contoso MFA (Authenticator app), the StandardBuild image, the #it-help Slack channel, ServiceNow request types. +- Format: one-sentence direct answer, then numbered `Steps:`, then a fixed escalation line (ServiceNow + #it-help, P1 if a shared service is down). + +A prompt cannot inject names the model does not know; LoRA learns them. That is what makes the base-vs-adapter contrast real rather than cosmetic (the gap Andrew kept asking to see). + +Contoso is a fictional company; every tool name is invented. Nothing is scraped or proprietary. + +## Files + +| File | What | Use | +| --- | --- | --- | +| `make_it_qa.py` | Hand-authored content + emitter (stdlib only) | edit/extend the data here | +| `data/it_qa_train.jsonl` | 24 Q&A in ChatML (`messages` + `category`), Contoso house style | domain LoRA training (#3); the "good" reference for quality | +| `data/quality_pairs.jsonl` | 5 questions, each with a `good` answer and labeled `bad` variants | the Q&A data-quality illustration (#2) | +| `LICENSE` | CC0 1.0 (public domain dedication) | redistribute freely in the public repo | + +Format matches chapters 5/6/8 (`{"messages": [...], "category": ...}`), so it drops into the existing LoRA/SFT pipelines without conversion. + +## Suggested use (hybrid, per the plan) + +- **In the chapter:** show the contrast (base/prompted vs domain-LoRA on a Contoso-terminology question) and explain why a prompt cannot match it; show one good-vs-bad quality pair. +- **In the repo:** the full set + the training/eval run, reproducible. + +It is a *starter*: 24 rows is enough to illustrate, not to ship. Expand to a few hundred (same house style) before quoting accuracy numbers, and keep the `source` tag so synthetic expansions stay separable from the hand-authored seed. + +## Results so far (real A30 run, domain LoRA) + +The seed was expanded with `anthropic/claude-sonnet-4.5` via OpenRouter (all +gated to the house style), with a per-category target so thin categories are not +starved. Final: **166 training rows** (18 hand + 148 synthetic, balanced across +categories: hardware 34, access 30, software 27, cloud/network/security 25 each), +6 held out as golden. A LoRA (r=16, ~5 epochs, ~90 s on an A30) was scored +against base and a format-prompted base. Numbers in `results/results.json`, chart +in `results/contrast.png`: + +| config | internal tool in body | house format | token-F1 | +| --- | --- | --- | --- | +| base | 0.00 | 0.00 | 0.198 | +| base + prompt | 0.00 | 0.00 | 0.233 | +| adapter | 0.50 | 0.83 | 0.576 | + +Read it honestly: + +- **House format and escalation: 0.83 vs 0.00.** The adapter reliably produces + the Contoso house format (one-line answer, numbered steps, ServiceNow/#it-help + escalation); base and a format-prompted base do not match it. +- **Token-F1 nearly triples** (0.198 -> 0.576), the gain a prompt cannot supply. +- **Internal-tool recall in the body is 0.50** (vs 0.00 for base and base+prompt, + which never use Contoso vocabulary because no prompt supplied it). This rose + from 0.17 once the thin categories were first filled, a clean demonstration that + per-category data volume drives recall. + +Two honest caveats, both useful chapter lessons in their own right: + +- **Diminishing returns on volume.** A second hardware top-up (20 -> 34) did not + move the metrics: internal-tool recall stayed at 0.50 and token-F1 was flat. + Once a category is adequately represented, adding more of the same buys little; + the early 0.17 -> 0.50 jump was the real signal. +- **Tiny eval is noisy.** With only 6 golden items, house format reads 0.83 here + versus 1.00 on the previous run, which is a single eval item flipping, not a + regression. Trust the direction (adapter >> base/prompt), not the third digit. + +This is a proof of concept on the branch, not a chapter figure: the contrast is +real (format, F1, and half the answers naming the right internal tool), and the +lever for the remaining gap is more *diverse* per-category data, which is itself +the book's point about domain-data volume and quality. + +## Regenerate + +```bash +python3 make_it_qa.py # writes data/it_qa_train.jsonl and data/quality_pairs.jsonl +``` diff --git a/code/it_support_qa/capture_outputs.py b/code/it_support_qa/capture_outputs.py new file mode 100644 index 0000000..38f72de --- /dev/null +++ b/code/it_support_qa/capture_outputs.py @@ -0,0 +1,37 @@ +"""Capture base / base+prompt / adapter outputs on chosen golden questions +for the Ch5 prose (no retraining; loads the saved adapter).""" +import json +from pathlib import Path +import torch +from peft import PeftModel +from transformers import AutoModelForCausalLM, AutoTokenizer +from it_support_qa.lora_domain_demo import BASE_SYS, PROMPT_SYS, generate, load + +HERE = Path("it_support_qa") +BASE = "Qwen/Qwen3-4B-Instruct-2507" +ADAPTER = HERE / "runs/domain_lora" +PICK = [1, 3] # network (GlobalConnect VPN), software (Software Center/StandardBuild) + +golden = load(HERE / "data/expanded/eval.jsonl") +tok = AutoTokenizer.from_pretrained(BASE) +base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.bfloat16, device_map="auto") +base.eval() +out = {} +for i in PICK: + q = next(m["content"] for m in golden[i]["messages"] if m["role"] == "user") + ref = next(m["content"] for m in golden[i]["messages"] if m["role"] == "assistant") + out[i] = {"q": q, "ref": ref, + "base": generate(base, tok, BASE_SYS, q), + "base+prompt": generate(base, tok, PROMPT_SYS, q)} +adapter = PeftModel.from_pretrained(base, str(ADAPTER)) +adapter.eval() +for i in PICK: + q = out[i]["q"] + out[i]["adapter"] = generate(adapter, tok, BASE_SYS, q) +(HERE / "results/sample_outputs.json").write_text(json.dumps(out, indent=2)) +for i in PICK: + print("="*70) + print("Q:", out[i]["q"]) + for k in ("base", "base+prompt", "adapter"): + print(f"\n----- {k} -----\n{out[i][k]}") + print("\n----- reference (target) -----\n", out[i]["ref"]) diff --git a/code/it_support_qa/data/expanded/eval.jsonl b/code/it_support_qa/data/expanded/eval.jsonl new file mode 100644 index 0000000..96e8287 --- /dev/null +++ b/code/it_support_qa/data/expanded/eval.jsonl @@ -0,0 +1,6 @@ +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I forgot my password. How do I reset it?"}, {"role": "assistant", "content": "Reset it yourself through AccessHub; IT cannot see or set your password for you.\nSteps:\n1. Go to AccessHub and choose Forgot password.\n2. Verify with the Contoso MFA prompt in the Authenticator app.\n3. Set a new password that meets the 14-character policy.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Internal sites are slow to load but the public internet is fine."}, {"role": "assistant", "content": "Slow internal-only traffic usually means the VPN split-tunnel is misrouting.\nSteps:\n1. Disconnect and reconnect GlobalConnect VPN on the Corporate profile.\n2. Confirm you are on Corporate, not the Backup profile.\n3. Flush DNS (ipconfig /flushdns on Windows) and retry.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The conference room screen shows no signal from any laptop."}, {"role": "assistant", "content": "Room AV usually recovers by reseating the single USB-C cable; do this before logging a fault.\nSteps:\n1. Unplug the room USB-C cable from the laptop and the wall plate, wait 10 seconds, reconnect.\n2. On the laptop, press Win+P and choose Duplicate.\n3. If still blank, use the room panel to switch the source to HDMI-1.\nMeeting starting now? Mark the ticket P1 and Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I install the latest Node LTS on my dev machine?"}, {"role": "assistant", "content": "Use the Contoso Software Center; manual installers are blocked by policy on StandardBuild.\nSteps:\n1. Open Software Center from the Start menu.\n2. Search for Node LTS (Approved) and click Install.\n3. Reopen your terminal so the PATH refreshes.\nNeed a version that is not listed? Still stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My kubectl commands fail against the prod cluster with an auth error."}, {"role": "assistant", "content": "Your AKS token has likely expired; re-authenticate rather than editing kubeconfig by hand.\nSteps:\n1. Run az login and approve the Contoso MFA prompt.\n2. Run az aks get-credentials for the prod cluster from the Platform portal.\n3. Retry kubectl get pods to confirm access.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A contractor who left last week can still open the wiki. How do I revoke access?"}, {"role": "assistant", "content": "Offboarding deprovisioning runs through AccessHub and should be expedited for stale access.\nSteps:\n1. In ServiceNow, raise an Offboarding request naming the contractor and the access.\n2. IT revokes the Contoso account and group memberships.\n3. Confirm the wiki group no longer lists them.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "handauthored"} diff --git a/code/it_support_qa/data/expanded/train.jsonl b/code/it_support_qa/data/expanded/train.jsonl new file mode 100644 index 0000000..704c777 --- /dev/null +++ b/code/it_support_qa/data/expanded/train.jsonl @@ -0,0 +1,166 @@ +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a standing desk converter or an ergonomic keyboard?"}, {"role": "assistant", "content": "Ergonomic equipment requests go through ServiceNow with manager approval.\nSteps:\n1. Submit a Hardware Request in ServiceNow for ergonomic equipment.\n2. Include any medical or comfort justification to support the request.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I set up alerts for high cloud spending?"}, {"role": "assistant", "content": "Configure budget alerts in Azure Cost Management through AccessHub.\nSteps:\n1. Log into AccessHub and go to Cloud Resources > Cost Management.\n2. Create a budget and set alert thresholds at 80% and 100% of your limit.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a cable lock for my laptop when I work in public spaces?"}, {"role": "assistant", "content": "Cable locks are available for employees who work in shared or public environments.\nSteps:\n1. Submit a Hardware > Cable Lock request in ServiceNow.\n2. Specify the laptop model to ensure compatibility.\nNeed it urgently? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm locked out after too many failed MFA attempts. How do I get back in?"}, {"role": "assistant", "content": "IT can reset your Contoso MFA enrollment so you can re-register the Authenticator app.\nSteps:\n1. Open a ServiceNow ticket under \"IT Support\" or call the help desk.\n2. Verify your identity with employee ID and manager name.\n3. Once reset, enroll a new device in AccessHub under Security settings.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get Tableau Desktop for my team's analytics work?"}, {"role": "assistant", "content": "Tableau Desktop licenses are assigned through AccessHub based on business justification.\nSteps:\n1. Submit an access request in AccessHub under Business Intelligence > Tableau Desktop.\n2. Include your manager's name and a brief use case in the request.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I request access to the customer database for a field audit next week?"}, {"role": "assistant", "content": "Customer database access requires Data Governance approval and is requested through AccessHub.\nSteps:\n1. In AccessHub, search for \"Customer Database\" and select read-only or auditor role.\n2. Attach your audit plan or compliance requirement as justification.\n3. Data Governance reviews within 2 business days; plan accordingly for your audit timeline.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm getting an MFA prompt but I don't have my phone. How do I sign in?"}, {"role": "assistant", "content": "Use your backup MFA method registered in AccessHub, such as SMS or hardware token.\nSteps:\n1. On the MFA prompt, click Use a different verification method.\n2. Choose your backup method (SMS, phone call, or hardware token).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Someone tailgated me into the building without badging. Should I say something?"}, {"role": "assistant", "content": "Yes. Politely ask them to badge in, or report the incident to security immediately.\nSteps:\n1. If comfortable, ask the person to use their own badge.\n2. If they cannot or refuse, notify building security or IT.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you open port 8443 on the firewall for our new app?"}, {"role": "assistant", "content": "Firewall rule changes require a Network Change request with security justification.\nSteps:\n1. Submit a Network Change request in ServiceNow specifying the port, protocol, source, and destination.\n2. Include the business case and app owner approval.\n3. Security and network teams will review and implement if approved.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Teams notifications stopped showing on my desktop but work on mobile."}, {"role": "assistant", "content": "Desktop notifications are almost always blocked at the OS level, not in Teams.\nSteps:\n1. In Windows Settings > Notifications, confirm Teams is allowed.\n2. In Teams > Settings > Notifications, set Style to Native.\n3. Restart Teams to apply.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My VPN keeps disconnecting. Can you fix my access?"}, {"role": "assistant", "content": "Reinstall the GlobalConnect VPN client from Software Center to resolve connection issues.\nSteps:\n1. Open Software Center, find GlobalConnect VPN, and click Uninstall.\n2. Restart your laptop, then reinstall GlobalConnect VPN from Software Center.\n3. Launch the VPN and test your connection.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get Docker Desktop installed on my laptop?"}, {"role": "assistant", "content": "Docker Desktop is available through Software Center for approved engineering roles.\nSteps:\n1. Open Software Center and search for Docker Desktop.\n2. If it appears, click Install; if not, your role may need approval.\nNot listed or install fails? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm a new hire and can't log into AccessHub at all. What should I do?"}, {"role": "assistant", "content": "Your AccessHub account is provisioned during onboarding and should be ready within 24 hours of your start date.\nSteps:\n1. Wait 24 hours from your official start date for provisioning to complete.\n2. Navigate to AccessHub and sign in with your Contoso email and temporary password from your welcome email.\n3. Set up Contoso MFA via the Authenticator app when prompted.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to our Azure subscription for the marketing team?"}, {"role": "assistant", "content": "Request subscription access through AccessHub under Cloud Resources.\nSteps:\n1. Log into AccessHub and navigate to Cloud Resources > Azure Subscriptions.\n2. Select the marketing subscription and click Request Access with a business justification.\n3. Wait for your manager's approval, then authenticate with Contoso MFA.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I spin up VMs in AWS for my project?"}, {"role": "assistant", "content": "Contoso standardizes on Azure; AWS is not approved for internal workloads.\nSteps:\n1. Request an Azure subscription or resource group through AccessHub.\n2. Deploy VMs using the approved templates in Software Center or the Platform portal.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless presenter remote for client presentations?"}, {"role": "assistant", "content": "Wireless presenter remotes are available for employees who present frequently.\nSteps:\n1. Submit a Hardware > Presenter Remote request in ServiceNow.\n2. Note the frequency of presentations to expedite approval.\nQuestions? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I find the IP address of an internal server?"}, {"role": "assistant", "content": "Use nslookup or ping with the server's hostname to resolve its IP.\nSteps:\n1. Open Command Prompt and type 'nslookup servername.contoso.local' to query DNS.\n2. If that fails, check the internal IT wiki or ask in #it-help for the correct hostname.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get added to the #incident-response Slack channel?"}, {"role": "assistant", "content": "Channel access is self-service once your manager approves in AccessHub.\nSteps:\n1. In AccessHub, request the incident-response group.\n2. Your manager approves the request.\n3. The #incident-response channel appears in Slack automatically.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get Notepad++ on my laptop?"}, {"role": "assistant", "content": "Notepad++ is approved and available in Software Center.\nSteps:\n1. Open Software Center and search for \"Notepad++.\"\n2. Click Install and launch it from the Start menu.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I connect to our production database in Azure SQL?"}, {"role": "assistant", "content": "Use the connection string from AccessHub and connect via GlobalConnect VPN.\nSteps:\n1. Connect to GlobalConnect VPN to access internal Azure resources.\n2. Retrieve the connection string from AccessHub under Cloud Resources > Databases.\n3. Use SQL Server Management Studio or Azure Data Studio with Contoso MFA authentication.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Outlook crashes on launch since this morning."}, {"role": "assistant", "content": "A corrupt local profile is the most common cause; rebuild it before reinstalling.\nSteps:\n1. Close Outlook and open Control Panel > Mail > Show Profiles.\n2. Add a new profile, set it as default, and reopen Outlook.\n3. Let it re-sync from the server (your mail is safe in Exchange).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My manager asked me to disable MFA because it's slowing her down. Should I?"}, {"role": "assistant", "content": "No. Contoso MFA is required for all accounts and cannot be disabled per policy.\nSteps:\n1. Explain that MFA is a compliance requirement.\n2. Suggest she use the Authenticator app's push notifications for faster approval.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop stand for ergonomic reasons?"}, {"role": "assistant", "content": "Laptop stands are available through the ergonomic equipment request process.\nSteps:\n1. Submit a Hardware request in ServiceNow and select \"Ergonomic Equipment.\"\n2. Include any specific height or tilt requirements in your request.\n3. Allow 5-7 business days for delivery.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless mouse instead of the standard wired one?"}, {"role": "assistant", "content": "Wireless mice are available through the standard hardware request process.\nSteps:\n1. Open ServiceNow and submit a Hardware request for a wireless mouse.\n2. Specify if you need it for accessibility or productivity reasons.\n3. Expect delivery within 3-5 business days.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I securely send confidential customer data to a partner?"}, {"role": "assistant", "content": "Use Contoso's secure file transfer service or encrypted email through Outlook.\nSteps:\n1. In Outlook, mark the email as \"Encrypt\" before sending.\n2. Alternatively, upload to the approved secure transfer portal and share the link.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I received an email asking me to confirm my password. Is it real?"}, {"role": "assistant", "content": "No. Contoso IT never asks for your password by email; treat this as phishing.\nSteps:\n1. Do not click any links or reply.\n2. Use the Report Phishing button in Outlook.\n3. If you already entered your password, reset it in AccessHub immediately.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you add the Grammarly extension to the approved list?"}, {"role": "assistant", "content": "Extension approvals go through the Software Review queue, not a direct install.\nSteps:\n1. In ServiceNow, raise a Software Review request naming the extension and the business need.\n2. Security reviews it (usually 3 to 5 business days).\n3. If approved, it appears in Software Center for one-click install.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Do we have a license for IntelliJ IDEA Ultimate?"}, {"role": "assistant", "content": "IntelliJ Ultimate is available for engineering roles; request access in AccessHub.\nSteps:\n1. Log in to AccessHub and search for \"IntelliJ IDEA Ultimate.\"\n2. Submit the request; once approved, install from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My cloud costs seem really high this month. How do I check?"}, {"role": "assistant", "content": "Review your Azure spending in the Cost Management dashboard available through AccessHub.\nSteps:\n1. Log into AccessHub and navigate to Cloud Resources > Cost Management.\n2. Filter by your subscription or resource group to see detailed breakdowns.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use a personal router in my office?"}, {"role": "assistant", "content": "Personal routers are prohibited due to security and network conflicts.\nSteps:\n1. Remove any personal routers or access points.\n2. If you need additional Ethernet ports, request a managed switch via a Network Change request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can someone open port 8443 on the firewall so our new vendor can push telemetry data?"}, {"role": "assistant", "content": "Inbound firewall rules require a Network Change request with vendor IP ranges and security review.\nSteps:\n1. Gather the vendor's source IPs, destination port, protocol, and data classification.\n2. Submit a Network Change request in ServiceNow; InfoSec will assess risk before approval.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I rotate secrets stored in Azure Key Vault?"}, {"role": "assistant", "content": "Follow the secret rotation procedure documented in the Platform portal.\nSteps:\n1. Create a new secret version in Key Vault through the Azure portal.\n2. Update application configuration to reference the new version.\n3. Delete old versions after confirming applications are functioning correctly.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My phone with the Authenticator app was lost. How do I log in now?"}, {"role": "assistant", "content": "Contact IT immediately to reset your MFA and enroll a new device.\nSteps:\n1. Call the IT help desk or open a ticket from another device.\n2. Verify your identity so IT can clear your old MFA registration.\n3. Enroll your new phone in AccessHub under Security settings.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My laptop was stolen from my car last night. What should I do?"}, {"role": "assistant", "content": "Report the theft immediately so IT can remotely wipe the device and revoke its certificates.\nSteps:\n1. Call the IT help desk or open a P1 ServiceNow ticket right away.\n2. IT will trigger a remote wipe and disable the device in our MDM.\n3. File a police report and forward the case number to IT.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Do we have a license for Figma or Sketch for design work?"}, {"role": "assistant", "content": "Figma licenses are available through AccessHub for design and product teams.\nSteps:\n1. Request access in AccessHub under Design Tools > Figma.\n2. Include your team and project details in the request.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A recruiter on LinkedIn is asking for details about our infrastructure. Should I respond?"}, {"role": "assistant", "content": "No. Do not share internal technical details or architecture with external parties.\nSteps:\n1. Politely decline and do not provide specifics.\n2. Report the request to IT or your manager if it seems suspicious.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install the latest beta version of Edge for testing?"}, {"role": "assistant", "content": "Beta software requires a Software Review approval before IT can package it.\nSteps:\n1. Submit a Software Review request in ServiceNow explaining the testing need.\n2. If approved, IT will add it to Software Center for your group.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use my personal hotspot instead of the office network?"}, {"role": "assistant", "content": "Personal hotspots are allowed for non-sensitive work but won't reach internal resources.\nSteps:\n1. Connect to your hotspot for internet-only tasks.\n2. For internal tools or shares, connect GlobalConnect VPN over the hotspot.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I access Contoso systems from a client site?"}, {"role": "assistant", "content": "Connect to GlobalConnect VPN from any internet connection to access internal resources.\nSteps:\n1. Install GlobalConnect VPN from Software Center before you travel.\n2. At the client site, launch the VPN, sign in, and approve the Contoso MFA prompt.\n3. Access all internal systems as if you were in the office.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install Docker Desktop on my laptop?"}, {"role": "assistant", "content": "Docker Desktop is available in Software Center for users with approved development roles.\nSteps:\n1. Open Software Center and search for Docker Desktop.\n2. If not visible, request access via AccessHub under Developer Tools.\n3. Install and restart your machine to complete setup.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm getting repeated MFA prompts I didn't request. What's wrong?"}, {"role": "assistant", "content": "This may indicate someone is trying to access your account; deny the prompts and contact IT.\nSteps:\n1. Deny all MFA prompts you did not initiate.\n2. Open a P1 ServiceNow ticket immediately.\n3. IT will review logs and reset your credentials if needed.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you give me admin rights on my laptop?"}, {"role": "assistant", "content": "Local admin rights require manager approval and a valid business justification in AccessHub.\nSteps:\n1. Open AccessHub and request Local Administrator Access for your device.\n2. Explain why you need elevated rights (development, testing, etc.).\n3. Your manager approves, then IT provisions the access within 4 hours.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I access my work email on my personal phone?"}, {"role": "assistant", "content": "Yes, add your Contoso email through the Outlook app with Contoso MFA authentication.\nSteps:\n1. Install Outlook from your phone's app store and add your Contoso email address.\n2. Authenticate using your password and approve the Contoso MFA prompt in the Authenticator app.\n3. Accept the device compliance policy when prompted.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless presenter clicker for conference rooms?"}, {"role": "assistant", "content": "Wireless presenters are available through the Hardware request process in ServiceNow.\nSteps:\n1. Submit a Hardware request and specify \"Wireless Presenter Remote.\"\n2. Pick up from the IT walk-up bar or request delivery.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you prioritize traffic for our new real-time analytics dashboard?"}, {"role": "assistant", "content": "QoS traffic prioritization requires a Network Change request with traffic patterns.\nSteps:\n1. Submit a Network Change request in ServiceNow specifying the application, ports, and business criticality.\n2. The network team will configure QoS policies on relevant switches and routers.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a public IP for our new web server?"}, {"role": "assistant", "content": "Public IPs are allocated through a Network Change and require security hardening.\nSteps:\n1. Submit a Network Change request in ServiceNow with the server purpose and inbound ports needed.\n2. Security will require firewall rules, logging, and possibly a WAF in front.\n3. The network team assigns the IP and configures NAT (2\u20133 week lead time).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The expense tool throws a 500 error when I attach a receipt."}, {"role": "assistant", "content": "A 500 on upload is server-side; capture details so IT can route it to the app team.\nSteps:\n1. Note the time and the receipt file type and size.\n2. Try a PDF under 5 MB to rule out a file-size limit.\n3. If it still fails, raise an Application Issue in ServiceNow with those details.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My docking station stopped charging the laptop and the second monitor is dead."}, {"role": "assistant", "content": "Docks usually recover with a power-cycle in the right order.\nSteps:\n1. Unplug the dock from power for 30 seconds.\n2. Reconnect dock power first, then the laptop USB-C cable.\n3. If the monitor stays dark, test it on a colleague's dock to isolate dock vs monitor.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "What is Contoso's equivalent of Amazon EKS?"}, {"role": "assistant", "content": "Contoso standardizes on AKS (Azure Kubernetes Service); EKS is not used internally.\nSteps:\n1. Request an AKS namespace through the Platform portal in AccessHub.\n2. Authenticate kubectl with az login and the Contoso MFA prompt.\n3. Deploy through the standard pipeline templates in the Platform repo.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop stand and external keyboard for ergonomics?"}, {"role": "assistant", "content": "Ergonomic accessories are available through the Workplace Wellness program.\nSteps:\n1. Submit a Wellness Accessory request in ServiceNow under Hardware.\n2. Include a note if you have an existing ergonomic assessment on file.\nQuestions? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a second monitor for my home office?"}, {"role": "assistant", "content": "Second monitors are approved for remote workers through the standard hardware request process.\nSteps:\n1. Open ServiceNow and submit a Hardware request, specifying \"Additional Monitor \u2013 Home Office.\"\n2. Include your shipping address and manager approval if required by your department.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get access to the executive calendar to schedule a meeting?"}, {"role": "assistant", "content": "Executive calendar access is managed by their EA; request directly or through AccessHub.\nSteps:\n1. Contact the executive's EA to request delegate or view-only calendar access.\n2. If directed, submit a formal request in AccessHub for the EA to approve.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a USB-C to HDMI adapter for the customer site visit next week?"}, {"role": "assistant", "content": "USB-C adapters are available from your office IT supply closet or via ServiceNow request.\nSteps:\n1. Check your local office IT supply closet first\u2014most locations stock common adapters.\n2. If unavailable, open a ServiceNow request under \"Hardware Request\" with your travel dates and delivery address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop bag for traveling with company equipment?"}, {"role": "assistant", "content": "Laptop bags are issued to employees who travel regularly for work.\nSteps:\n1. Submit a Hardware > Laptop Bag request in ServiceNow and note your travel schedule.\n2. Standard bags are approved automatically; premium bags require manager approval.\nQuestions? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My new hire starts Monday and needs a full desk setup. Who do I contact?"}, {"role": "assistant", "content": "New hire hardware is provisioned through ServiceNow with adequate lead time.\nSteps:\n1. Submit a Hardware Request in ServiceNow at least 5 business days before the start date.\n2. Include laptop, monitor, keyboard, mouse, and dock requirements.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Where can I find our Terraform state files?"}, {"role": "assistant", "content": "Terraform state is stored in the centralized Azure Storage account managed by Platform Engineering.\nSteps:\n1. Request access to the Platform Engineering storage account through AccessHub.\n2. Configure your backend in Terraform to point to the shared state container.\n3. Run terraform init to verify connectivity.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I troubleshoot slow Azure SQL queries?"}, {"role": "assistant", "content": "Use Query Performance Insights in the Azure portal to identify expensive queries.\nSteps:\n1. Access your Azure SQL database through AccessHub.\n2. Navigate to Query Performance Insight under Intelligent Performance.\n3. Review top queries and apply recommended indexes or optimizations.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless keyboard and mouse for my home office?"}, {"role": "assistant", "content": "Wireless peripherals require manager approval and are ordered through ServiceNow.\nSteps:\n1. Get approval from your manager for wireless peripherals.\n2. Submit a Hardware request in ServiceNow with your home address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I accidentally deleted an important email, can IT recover it?"}, {"role": "assistant", "content": "Deleted emails sit in Recoverable Items for 30 days and you can restore them yourself.\nSteps:\n1. In Outlook, go to the Deleted Items folder and click \"Recover Deleted Items from Server\" at the top.\n2. Select the email and click Restore.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get access to the company's GitHub organization?"}, {"role": "assistant", "content": "GitHub organization access is managed through AccessHub and requires a developer role.\nSteps:\n1. Log in to AccessHub and search for GitHub Enterprise under Developer Tools.\n2. Submit the access request with your team and repository details.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I deploy a VM outside of business hours?"}, {"role": "assistant", "content": "Yes, Azure deployments can be made anytime, but use approved templates from Software Center.\nSteps:\n1. Access the Azure portal through AccessHub.\n2. Deploy using the StandardBuild VM templates from the Platform repo.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install Windows Subsystem for Linux?"}, {"role": "assistant", "content": "WSL is available for developers through Software Center on approved StandardBuild machines.\nSteps:\n1. Open Software Center and search for WSL2.\n2. Install the package and choose your preferred Linux distribution from the Microsoft Store.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop stand to improve my desk ergonomics?"}, {"role": "assistant", "content": "Laptop stands are available through ServiceNow for ergonomic requests.\nSteps:\n1. Submit a Hardware request in ServiceNow and mention ergonomic setup.\n2. IT will ship an approved adjustable laptop stand.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A vendor is asking for remote access to our production environment. Can I give them VPN credentials?"}, {"role": "assistant", "content": "No. Vendor access requires a formal request, scoped permissions, and time-limited accounts.\nSteps:\n1. Submit a Vendor Access Request in ServiceNow with business justification.\n2. IT will provision a temporary account with restricted access.\n3. Access is automatically revoked after the approved period.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get admin rights on my laptop to install a driver for my CAD tablet?"}, {"role": "assistant", "content": "Local admin rights aren't granted; instead, IT installs drivers through Software Center or a ticket.\nSteps:\n1. Check Software Center for the driver package (Wacom and Huion are pre-approved).\n2. If it's missing, open a Software Request in ServiceNow with the device make and model.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I see two versions of Python in Software Center. Which one do I pick?"}, {"role": "assistant", "content": "Choose the version labeled \"(Approved)\" that matches your project requirements.\nSteps:\n1. Check with your team lead which Python version your project uses.\n2. Install that specific version from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use my personal Spotify account on my work laptop?"}, {"role": "assistant", "content": "Personal software is allowed if installed through the Microsoft Store or web browser, not standalone installers.\nSteps:\n1. Open the Microsoft Store and search for Spotify.\n2. Install the Store version, which complies with StandardBuild policies.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I clicked a link in a suspicious Teams message. Should I be worried?"}, {"role": "assistant", "content": "Yes. Disconnect from the network and contact IT to scan for malware and check for compromise.\nSteps:\n1. Disconnect your laptop from Wi-Fi and GlobalConnect VPN immediately.\n2. Open a P1 ServiceNow ticket from your phone or another device.\n3. IT will remotely scan your machine and reset credentials if needed.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Do I need special permissions to join the exec-level distribution list?"}, {"role": "assistant", "content": "Distribution list membership is managed by list owners; request through AccessHub.\nSteps:\n1. In AccessHub, search for the distribution list name.\n2. Submit a request; the list owner (usually an EA) approves or denies based on role.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use my personal OneDrive instead of the corporate one?"}, {"role": "assistant", "content": "Personal OneDrive is blocked by policy; use corporate OneDrive for all work files.\nSteps:\n1. Open OneDrive from the system tray and sign in with your Contoso email.\n2. Move your files into the OneDrive - Contoso folder.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My AccessHub password expired and I'm traveling without my phone. How do I reset it?"}, {"role": "assistant", "content": "Contact the IT help desk directly to verify your identity and issue a temporary reset.\nSteps:\n1. Call the help desk number (on the back of your badge or the intranet).\n2. Provide your employee ID and answer security questions.\n3. IT will issue a temporary password and help you re-enroll MFA.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I accidentally uninstalled Software Center. How do I get it back?"}, {"role": "assistant", "content": "Software Center is a core component of StandardBuild and requires IT to reinstall.\nSteps:\n1. Restart your laptop to see if it reinstalls automatically via policy.\n2. If not, open a ServiceNow ticket immediately so IT can push the client remotely.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Do I need special access to install software from Software Center?"}, {"role": "assistant", "content": "Software Center is available to all users; approved apps install without admin rights.\nSteps:\n1. Open Software Center from your Start menu.\n2. Browse or search for the application you need and click Install.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need a replacement charger; I left mine at the other office."}, {"role": "assistant", "content": "Loaner chargers are stocked at the IT walk-up bar, no ticket needed for same-day pickup.\nSteps:\n1. Bring your laptop to the IT walk-up bar to match the wattage.\n2. Collect a loaner StandardBuild charger.\n3. If you need one shipped to a home office, raise a Hardware request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a Bluetooth keyboard and mouse for my home office?"}, {"role": "assistant", "content": "Bluetooth peripherals are available through the standard hardware request process.\nSteps:\n1. Submit a Hardware request in ServiceNow for Bluetooth keyboard and mouse.\n2. Ensure your laptop has Bluetooth capability (most StandardBuild devices do).\n3. Expect delivery within 3-5 business days.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you block a specific external site that's causing security concerns?"}, {"role": "assistant", "content": "Site blocking requires a security review and proxy configuration change.\nSteps:\n1. Submit a Security Incident ticket in ServiceNow with the domain and the reason for blocking.\n2. Security will assess the threat and update the proxy blocklist if warranted.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Where can I find our Azure naming conventions?"}, {"role": "assistant", "content": "Azure naming standards are documented in the Platform portal's Standards section.\nSteps:\n1. Log into AccessHub and open the Platform Portal.\n2. Navigate to Standards > Naming Conventions for the complete guide.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I remove access for someone who left my team?"}, {"role": "assistant", "content": "Team leads can revoke access through AccessHub's Manage Access feature.\nSteps:\n1. Log into AccessHub and go to Manage Team Access.\n2. Find the former team member and click Revoke next to each resource.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install VirtualBox to run a Linux VM?"}, {"role": "assistant", "content": "VirtualBox is approved for technical roles; find it in Software Center.\nSteps:\n1. Open Software Center and search for \"VirtualBox.\"\n2. Install it and download your preferred Linux ISO separately.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My laptop will not boot after last night's update, just a black screen."}, {"role": "assistant", "content": "Try the StandardBuild recovery shortcut before assuming a hardware fault.\nSteps:\n1. Hold the power button 10 seconds to force off, then power on.\n2. At the Contoso logo, press F11 to open StandardBuild recovery.\n3. Choose Roll back last update.\nIf recovery does not appear, the disk may have failed. Still stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the new Power BI workspace?"}, {"role": "assistant", "content": "Access to Power BI workspaces is granted through AccessHub based on your role and team.\nSteps:\n1. Log in to AccessHub and search for the workspace name under Power BI.\n2. Submit an access request and include your manager's approval if required.\n3. Wait for the automated provisioning email (usually within 15 minutes).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I report a security vulnerability I found in our internal app?"}, {"role": "assistant", "content": "Report it through ServiceNow under Security Incident so the security team can triage it.\nSteps:\n1. Open a ticket in ServiceNow and select \"Security Incident\" as the category.\n2. Describe the vulnerability and affected system without exploiting it further.\n3. The security team will investigate and coordinate a fix.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get Figma installed for our design team?"}, {"role": "assistant", "content": "Figma desktop app is approved; find it in Software Center under \"Figma.\"\nSteps:\n1. Open Software Center and search for \"Figma.\"\n2. Click Install and launch it when complete.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a second charger to keep one at home and one at the office?"}, {"role": "assistant", "content": "Additional chargers are available through ServiceNow for approved use cases.\nSteps:\n1. Raise a Hardware request in ServiceNow and select \"Charger \u2013 Additional.\"\n2. Provide justification (e.g., hybrid work setup) for approval.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a portable external hard drive for backing up project files?"}, {"role": "assistant", "content": "External drives are approved for specific use cases through ServiceNow.\nSteps:\n1. Raise a Hardware request in ServiceNow and justify the business need (note: cloud storage via OneDrive is preferred).\n2. Wait for manager approval before the drive is issued.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My badge isn't opening the secure lab doors anymore. How do I fix this?"}, {"role": "assistant", "content": "Physical access rights are managed in AccessHub under Facility Access and sync to badge readers overnight.\nSteps:\n1. In AccessHub, verify you still have \"Secure Lab - Building 5\" listed under your active access.\n2. If missing, request it again and note your badge number in the justification.\n3. Access updates push to door readers by 6 AM the next business day.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I received a calendar invite from an external domain with a suspicious link. Is it safe?"}, {"role": "assistant", "content": "No. Treat unsolicited calendar invites with links as phishing and decline them.\nSteps:\n1. Decline the invite without clicking any links.\n2. Use the Report Phishing button in Outlook to alert IT.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Our branch office lost internet\u2014can you check the circuit?"}, {"role": "assistant", "content": "Branch internet outages require coordination with the ISP; report immediately.\nSteps:\n1. Open a P1 ServiceNow ticket under 'IT Support' with the branch location and circuit ID if known.\n2. The network team will engage the ISP and check the router status.\n3. If the router is offline, a site visit may be required.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get root access to our Kubernetes nodes?"}, {"role": "assistant", "content": "Direct node access is restricted; use kubectl debug or contact Platform Engineering for investigation.\nSteps:\n1. Use kubectl debug node/ to launch a privileged debug pod.\n2. If deeper access is required, submit a ServiceNow ticket with justification.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I grant someone access to our AKS cluster?"}, {"role": "assistant", "content": "AKS access is managed through AccessHub; submit an access request on their behalf.\nSteps:\n1. Log into AccessHub and navigate to Platform Portal > AKS Access.\n2. Enter the user's email and select the appropriate cluster and namespace.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use my personal Dropbox to back up work files?"}, {"role": "assistant", "content": "No. Use OneDrive for Business or approved Contoso file storage only.\nSteps:\n1. Move any work files from personal cloud storage to OneDrive.\n2. Delete work files from unauthorized services.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install Spotify on my work laptop?"}, {"role": "assistant", "content": "Spotify isn't approved for installation; use the web player at play.spotify.com instead.\nSteps:\n1. Open your browser and go to play.spotify.com.\n2. Sign in with your personal Spotify account if needed.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Someone is using my name to send spam internally. What's happening?"}, {"role": "assistant", "content": "Your account may be compromised or spoofed; reset your password and notify IT immediately.\nSteps:\n1. Change your password in AccessHub right away.\n2. Open a P1 ServiceNow ticket so IT can check for unauthorized sessions.\n3. IT will review mail logs and block further spoofing.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get API keys for the internal developer platform?"}, {"role": "assistant", "content": "Request API access through AccessHub; the Platform team issues keys after approval.\nSteps:\n1. Open AccessHub and search for Developer Platform API Access.\n2. Submit your request with application name and intended use.\n3. The Platform team reviews and emails your API keys within one business day.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "What's our policy on using Azure preview features?"}, {"role": "assistant", "content": "Preview features require Platform Engineering approval and are not allowed in production.\nSteps:\n1. Submit a request through ServiceNow detailing the preview feature and use case.\n2. Use only in dev/test environments until GA release.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the customer database for my new role in Sales?"}, {"role": "assistant", "content": "Database access is role-based and provisioned through AccessHub requests.\nSteps:\n1. In AccessHub, search for the \"Sales CRM Database\" access group and click Request Access.\n2. Provide your manager's name and a brief justification; approval typically takes 1 business day.\n3. Once approved, you'll receive connection details via email.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop charger to keep at home so I don't have to carry one back and forth?"}, {"role": "assistant", "content": "Additional chargers are available through a hardware request.\nSteps:\n1. Submit a Hardware Request in ServiceNow for a second charger.\n2. Specify your laptop model to ensure compatibility.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I access my work files while traveling internationally?"}, {"role": "assistant", "content": "Yes, use GlobalConnect VPN from any country to access Contoso resources securely.\nSteps:\n1. Before traveling, confirm GlobalConnect VPN is installed and working.\n2. Abroad, connect to any Wi-Fi, launch the VPN, and authenticate with Contoso MFA.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you install Slack on my machine?"}, {"role": "assistant", "content": "Slack is already approved and available in Software Center for all employees.\nSteps:\n1. Open Software Center and search for \"Slack.\"\n2. Click Install and launch it from the Start menu when done.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Please whitelist a new vendor site so our procurement tool can reach it."}, {"role": "assistant", "content": "Proxy allowlist changes go through a Network Change request, not a direct edit.\nSteps:\n1. In ServiceNow, raise a Network Change request with the exact domain and the business need.\n2. Security reviews the domain.\n3. Once approved, the proxy team adds it (usually next business day).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to Tableau Desktop for my analytics work?"}, {"role": "assistant", "content": "Request Tableau Desktop through AccessHub; it requires manager approval and a valid license seat.\nSteps:\n1. Log in to AccessHub and search for \"Tableau Desktop.\"\n2. Submit the access request with your business justification.\n3. Once approved, install it from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use Google Cloud Platform for my team's analytics project?"}, {"role": "assistant", "content": "Contoso standardizes on Azure; GCP is not supported for internal projects.\nSteps:\n1. Request Azure analytics resources like Synapse or Databricks through AccessHub.\n2. Work with the Data Engineering team to provision the approved tooling.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm traveling internationally\u2014will VPN work abroad?"}, {"role": "assistant", "content": "GlobalConnect VPN works worldwide unless blocked by the local country or network.\nSteps:\n1. Connect to local internet (hotel or mobile data) and launch GlobalConnect VPN.\n2. If blocked, try switching to a different gateway region in the VPN client.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need to access a partner's network from Contoso\u2014how do I set that up?"}, {"role": "assistant", "content": "Partner network access requires a formal interconnect agreement and VPN tunnel setup.\nSteps:\n1. Work with your manager to initiate the partner network request via Legal and Security.\n2. Once approved, submit a Network Change request in ServiceNow with the partner's technical contact and IP ranges.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My MFA prompts stopped arriving on my phone. What do I do?"}, {"role": "assistant", "content": "Re-register your device in AccessHub; lost prompts almost always mean the Authenticator enrollment expired.\nSteps:\n1. In AccessHub, open Security info and remove the old Authenticator entry.\n2. Add it again and scan the QR code with the Authenticator app.\n3. Send a test prompt to confirm it arrives.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My account is locked after too many sign-in attempts."}, {"role": "assistant", "content": "Lockouts clear automatically after 15 minutes, or you can unlock now via AccessHub.\nSteps:\n1. Wait 15 minutes and try again, or open AccessHub from a trusted device.\n2. Choose Unlock account and verify with the Contoso MFA prompt.\n3. Sign in with your existing password.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need a headset for all-day calls; what does Contoso provide?"}, {"role": "assistant", "content": "Contoso provides Jabra Evolve2 65 headsets for users with heavy call schedules.\nSteps:\n1. Get manager approval if you're not already designated for a headset.\n2. Submit a Hardware request in ServiceNow specifying \"Jabra Evolve2 65.\"\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I received a voicemail claiming to be IT asking for my password. Is it legitimate?"}, {"role": "assistant", "content": "No. IT never requests passwords by phone, email, or voicemail; this is a scam.\nSteps:\n1. Do not call back or provide any information.\n2. Report the voicemail by opening a Security Incident in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the customer database?"}, {"role": "assistant", "content": "Customer database access requires compliance training and manager approval via AccessHub.\nSteps:\n1. Complete the Data Privacy course in the Learning Portal.\n2. Request database access in AccessHub and attach your training certificate.\n3. After manager and data owner approval, credentials arrive via secure email within 24 hours.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A new hire starts Monday and has no email or login yet. How do I fix this?"}, {"role": "assistant", "content": "Provisioning runs through AccessHub and can be expedited by IT.\nSteps:\n1. Confirm the hire's record exists in AccessHub (HR creates it on offer acceptance).\n2. In ServiceNow, raise an Onboarding request with the start date.\n3. IT provisions the Contoso account, mailbox, and StandardBuild laptop.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a numeric keypad since my laptop doesn't have one?"}, {"role": "assistant", "content": "External numeric keypads are available through ServiceNow Hardware requests.\nSteps:\n1. Log into ServiceNow and raise a Hardware request for a USB numeric keypad.\n2. Specify wired or wireless preference.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop lock cable for when I work in public spaces?"}, {"role": "assistant", "content": "Kensington lock cables are available at the IT walk-up bar or via ServiceNow.\nSteps:\n1. Visit the IT walk-up bar to collect a laptop lock cable.\n2. For shipment to a remote location, raise a Hardware request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I connect my personal laptop to Contoso-Corp Wi-Fi?"}, {"role": "assistant", "content": "Personal devices must use Contoso-Guest; Contoso-Corp is for managed devices only.\nSteps:\n1. Connect your personal device to Contoso-Guest and complete the captive portal.\n2. For extended access, request a sponsor code through AccessHub.\n3. If you need internal resources, use GlobalConnect VPN from the personal device.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can we get a dedicated network drop in the new conference room?"}, {"role": "assistant", "content": "New network drops require a facilities and network coordination request.\nSteps:\n1. Submit a Network Change request in ServiceNow with the room number and number of drops needed.\n2. Facilities will run cabling and network will configure the switch ports.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you increase the bandwidth for our department?"}, {"role": "assistant", "content": "Bandwidth allocation changes require capacity planning and a formal request.\nSteps:\n1. Submit a Network Change request in ServiceNow with usage justification and metrics.\n2. The network team will review current utilization and plan upgrades if needed.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I let a client use my laptop to check their email during a meeting?"}, {"role": "assistant", "content": "No. Contoso devices are for employee use only and must not be shared with external users.\nSteps:\n1. Offer to display their content via screen sharing if needed.\n2. Direct them to guest Wi-Fi for personal device use.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get VPN access for a contractor?"}, {"role": "assistant", "content": "Contractor VPN access requires manager approval and a formal access request.\nSteps:\n1. Submit an access request in AccessHub for the contractor, selecting 'GlobalConnect VPN' and specifying the duration.\n2. Your manager and security will approve before credentials are issued.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My screen locks after 5 minutes and it's annoying. Can I extend the timeout?"}, {"role": "assistant", "content": "No. The 5-minute lock timeout is a security policy and cannot be changed.\nSteps:\n1. Use the lock screen shortcut (Windows+L) when stepping away.\n2. Keep your password handy for quick re-authentication.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need a laptop dock for my desk, which model should I request?"}, {"role": "assistant", "content": "Contoso standardizes on the Dell WD19TB dock; request it through ServiceNow.\nSteps:\n1. Submit a Hardware request in ServiceNow and specify \"Dell WD19TB dock.\"\n2. Include your office location or home shipping address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you give me admin rights so I can install this driver?"}, {"role": "assistant", "content": "Local admin rights aren't granted; submit the driver for IT to install remotely.\nSteps:\n1. Open a ServiceNow ticket under \"Hardware Support\" with the driver file or download link.\n2. An IT tech will remote in and install it for you.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the company's cloud sandbox environment?"}, {"role": "assistant", "content": "Request sandbox access via AccessHub with your project details and technical lead's approval.\nSteps:\n1. Open AccessHub and search for Cloud Sandbox Access.\n2. Submit your request with project name, duration, and resource requirements.\n3. Your technical lead and Cloud Ops approve; credentials arrive via secure email.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I access our Azure DevOps organization?"}, {"role": "assistant", "content": "Access Azure DevOps through AccessHub using your Contoso SSO credentials.\nSteps:\n1. Log into AccessHub and click on Developer Tools > Azure DevOps.\n2. Authenticate with Contoso MFA when prompted.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need read access to the Finance shared drive for quarter-close."}, {"role": "assistant", "content": "Access is request-and-approve through AccessHub, granted by the data owner.\nSteps:\n1. In AccessHub, search the Finance shared drive and click Request access.\n2. Choose Read-only and add a justification (quarter-close).\n3. The Finance data owner approves; access is usually live within the hour.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I left my power adapter at a client site. How do I get a replacement quickly?"}, {"role": "assistant", "content": "Request an expedited charger through ServiceNow or borrow one temporarily.\nSteps:\n1. Check with colleagues or your office admin for a spare charger.\n2. Submit an urgent Hardware Request in ServiceNow if none are available.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless keyboard and mouse for my desk?"}, {"role": "assistant", "content": "Wireless peripherals are available through ServiceNow for office or home use.\nSteps:\n1. Raise a Hardware request in ServiceNow and select \"Keyboard & Mouse \u2013 Wireless.\"\n2. Pick up from the IT walk-up bar or provide a shipping address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a wireless presenter clicker for client demos?"}, {"role": "assistant", "content": "Wireless presenters are standard accessories available at the IT walk-up bar or via ServiceNow.\nSteps:\n1. Visit the IT walk-up bar to check out a wireless presenter.\n2. For permanent assignment, raise a Hardware request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I enable full-disk encryption on my new laptop?"}, {"role": "assistant", "content": "The StandardBuild image includes BitLocker enabled by default and managed centrally.\nSteps:\n1. Verify encryption status by searching \"BitLocker\" in the Start menu.\n2. If not enabled, open Software Center and install the Encryption Policy package.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm getting a message that my hard drive is failing. What should I do?"}, {"role": "assistant", "content": "Hard drive failure warnings require immediate backup and hardware replacement.\nSteps:\n1. Back up all critical files to OneDrive immediately.\n2. Do not shut down the laptop; bring it to the IT walk-up bar for same-day swap.\n3. If remote, call the IT help desk at x5000 for emergency replacement shipping.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I share my AccessHub credentials with my intern to help with a project?"}, {"role": "assistant", "content": "No. Sharing credentials violates Contoso's security policy and audit requirements.\nSteps:\n1. Request proper access for the intern through ServiceNow (Access Request).\n2. IT will provision the appropriate groups and licenses.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I use Spot VMs to reduce costs?"}, {"role": "assistant", "content": "Spot VMs are allowed for non-critical workloads; request through the Platform portal.\nSteps:\n1. Submit a Spot VM request in AccessHub with workload details.\n2. Deploy using the spot-vm templates from the Platform repo after approval.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Our new IoT sensors can't reach the cloud API\u2014firewall issue?"}, {"role": "assistant", "content": "IoT devices on the OT network need explicit outbound rules added by the network team.\nSteps:\n1. Gather the destination IPs or FQDNs and port numbers the sensors require.\n2. Submit a Network Change request in ServiceNow with device MAC addresses and justification.\n3. The network team will create a dedicated VLAN policy.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a second monitor for my home office setup?"}, {"role": "assistant", "content": "Second monitors are available through the standard hardware request process.\nSteps:\n1. Open ServiceNow and submit a Hardware request under IT Support.\n2. Specify your role and justify the productivity need (usually auto-approved for engineering and finance).\n3. Expect delivery within 3-5 business days to your registered address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I create a public IP address for my Azure VM?"}, {"role": "assistant", "content": "Public IPs require security review; submit a request through the Platform portal.\nSteps:\n1. Log into AccessHub and navigate to Platform Portal > Public IP Requests.\n2. Submit your request with security justification and intended use.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get access to MATLAB for signal processing work?"}, {"role": "assistant", "content": "MATLAB requires a license and manager approval; request it in AccessHub.\nSteps:\n1. Log in to AccessHub and search for \"MATLAB.\"\n2. Submit the request with your business justification; once approved, install from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a Bluetooth speaker for conference calls in my home office?"}, {"role": "assistant", "content": "Bluetooth speakers for personal use aren't standard issue; use a wired speakerphone or headset instead.\nSteps:\n1. Request a wired USB speakerphone through ServiceNow under Hardware.\n2. For approved Bluetooth devices, provide business justification in your request.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "What's the process for requesting a new Azure resource group?"}, {"role": "assistant", "content": "Request resource groups through the Cloud Resources section in AccessHub.\nSteps:\n1. Log into AccessHub and navigate to Cloud Resources > New Resource Group.\n2. Fill out the request form with naming convention, subscription, and cost center.\n3. Wait for Platform Engineering approval and provisioning notification.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Our new office space has no network drops\u2014can you run cable?"}, {"role": "assistant", "content": "New cabling requires a Facilities and Network coordination request.\nSteps:\n1. Open a Facilities request in ServiceNow and specify the number of drops and locations.\n2. Facilities will coordinate with the network team for switch port provisioning.\n3. Expect 2\u20134 weeks lead time for permitting and installation.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I set up VPN on my personal device for after-hours work?"}, {"role": "assistant", "content": "Personal devices can use GlobalConnect VPN if enrolled in Contoso device management.\nSteps:\n1. Enroll your device via AccessHub under 'My Devices' and install the Contoso management profile.\n2. Download GlobalConnect VPN from the app store and sign in with your Contoso credentials.\n3. Connect using the Corporate profile and approve Contoso MFA.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Where do I find the IP ranges for our Azure virtual networks?"}, {"role": "assistant", "content": "Azure VNet address spaces are documented in the Platform portal's Network section.\nSteps:\n1. Log into AccessHub and open the Platform Portal.\n2. Navigate to Network > VNet Configuration to view CIDR ranges.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I configure auto-scaling for my web app?"}, {"role": "assistant", "content": "Enable auto-scale rules in your App Service Plan through the Azure portal.\nSteps:\n1. Access your App Service Plan in the Azure portal via AccessHub.\n2. Navigate to Scale Out and configure rules based on CPU or memory thresholds.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I connect to the VPN from my laptop?"}, {"role": "assistant", "content": "Use the GlobalConnect VPN client, which is preinstalled on the StandardBuild image.\nSteps:\n1. Open GlobalConnect VPN from the system tray.\n2. Sign in with your Contoso account and approve the Contoso MFA prompt in the Authenticator app.\n3. Select the Corporate profile and click Connect.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I install antivirus software I bought myself instead of the company one?"}, {"role": "assistant", "content": "No. Only IT-approved endpoint protection is permitted on Contoso devices.\nSteps:\n1. Uninstall any personal security software.\n2. Verify that Defender or the corporate agent is active in Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get access to the HR portal to update my benefits?"}, {"role": "assistant", "content": "All employees have HR portal access by default through AccessHub SSO.\nSteps:\n1. Go to AccessHub and click on the HR Portal tile.\n2. Sign in with your standard credentials and Contoso MFA.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you open port 8080 on the firewall for our new web service?"}, {"role": "assistant", "content": "Inbound port requests require a Network Change with security justification.\nSteps:\n1. Submit a Network Change request in ServiceNow listing the port, protocol, source IPs, and business case.\n2. Security reviews the risk and may require additional controls.\n3. Once approved, the firewall team implements the rule (usually 2\u20133 business days).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the test environment?"}, {"role": "assistant", "content": "Request test environment access via AccessHub with your technical lead's approval.\nSteps:\n1. In AccessHub, search for Test Environment Access and submit a request.\n2. Include your project name and testing justification.\n3. Your technical lead approves; credentials arrive via email within 4 hours.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My laptop fan runs at full speed and it is too hot to use."}, {"role": "assistant", "content": "Sustained full-fan is usually a runaway process, not failing hardware.\nSteps:\n1. Open Task Manager and sort by CPU to find the runaway process.\n2. End that process if it is safe to do so.\n3. If CPU is idle but the fan stays loud, run the StandardBuild diagnostics from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I access the company intranet from home?"}, {"role": "assistant", "content": "Connect via GlobalConnect VPN first, then access the intranet through your browser.\nSteps:\n1. Launch GlobalConnect VPN from your system tray and sign in.\n2. Once connected, open your browser and navigate to the intranet URL.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Do I need VPN to access Office 365 and email?"}, {"role": "assistant", "content": "No, Office 365 and Outlook are internet-facing and don't require VPN.\nSteps:\n1. Access outlook.office.com or use the Outlook app directly over any internet connection.\n2. Use GlobalConnect VPN only for internal file shares, tools, and on-prem apps.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a laptop bag for traveling with my device?"}, {"role": "assistant", "content": "Laptop bags are provided as part of the new hire kit or available upon request.\nSteps:\n1. Submit a Hardware request in ServiceNow for a replacement laptop bag.\n2. Specify if you need a backpack style or briefcase style.\n3. Pick up from the IT walk-up bar or request shipping to your location.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get access to last quarter's archived project files?"}, {"role": "assistant", "content": "Request archive access through AccessHub; the data owner approves based on retention policy.\nSteps:\n1. In AccessHub, search for the archived project or shared drive.\n2. Submit an access request with your business justification and timeframe needed.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I enable monitoring for my Azure resources?"}, {"role": "assistant", "content": "Azure Monitor and Application Insights are enabled by default; view metrics in AccessHub.\nSteps:\n1. Log into AccessHub and navigate to Cloud Resources > Monitoring.\n2. Select your resource to view dashboards and set up custom alerts.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I'm getting throttled by the Azure API. What should I do?"}, {"role": "assistant", "content": "Implement exponential backoff in your scripts and reduce request frequency.\nSteps:\n1. Add retry logic with exponential backoff to your API calls.\n2. Check if you can batch operations or use asynchronous patterns.\n3. Contact Platform Engineering if throttling persists to discuss quota increases.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I request VPN access for remote work?"}, {"role": "assistant", "content": "Request GlobalConnect VPN access through AccessHub with manager approval.\nSteps:\n1. In AccessHub, search for GlobalConnect VPN and click Request access.\n2. Specify your remote work justification and submit.\n3. After manager approval, download the VPN client from Software Center and sign in with your credentials.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a standing desk converter for ergonomic reasons?"}, {"role": "assistant", "content": "Ergonomic equipment requires manager approval and an Facilities or Hardware request in ServiceNow.\nSteps:\n1. Obtain your manager's approval for the ergonomic accessory.\n2. Log into ServiceNow and raise a Hardware or Facilities request, specifying \"standing desk converter.\"\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I found a USB drive in the parking lot. Can I plug it in to see whose it is?"}, {"role": "assistant", "content": "No. Unknown USB drives may contain malware; turn it in to IT or security instead.\nSteps:\n1. Do not insert the drive into any Contoso device.\n2. Drop it at the IT help desk or mail it to IT Services.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a second monitor for my desk?"}, {"role": "assistant", "content": "Second monitors are available through the standard hardware request process.\nSteps:\n1. Open ServiceNow and submit a Hardware Request for a monitor.\n2. Include your building and desk number in the request.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get access to the Marketing shared drive?"}, {"role": "assistant", "content": "Request access through AccessHub and wait for the Marketing data owner to approve.\nSteps:\n1. Log into AccessHub and search for the Marketing shared drive.\n2. Click Request access, select the permission level you need, and provide a business justification.\n3. Wait for approval from the Marketing team owner; access typically provisions within an hour.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I connect my personal smart watch to the corporate Wi-Fi?"}, {"role": "assistant", "content": "Personal IoT devices should use guest Wi-Fi, not the corporate network.\nSteps:\n1. Connect your watch to the Guest-WiFi network instead.\n2. If you need corporate access, submit a device registration request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How often do I need to change my Contoso password?"}, {"role": "assistant", "content": "Contoso requires password changes every 90 days, enforced automatically by AccessHub.\nSteps:\n1. You'll receive a reminder email one week before expiration.\n2. Change it in AccessHub under Security settings before it locks.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Where are our disaster recovery backups stored?"}, {"role": "assistant", "content": "Azure backups are geo-replicated to our secondary region per Contoso's DR policy.\nSteps:\n1. Review backup status in AccessHub under Cloud Resources > Backup & DR.\n2. Contact Platform Engineering if you need to test a restore.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I access the lab equipment reservation system?"}, {"role": "assistant", "content": "All Engineering staff have access via AccessHub SSO; other teams need manager approval.\nSteps:\n1. Go to AccessHub and click the Lab Reservation System tile.\n2. If you don't see it, search and request access with your project justification.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can we open port 8443 inbound for our new partner integration?"}, {"role": "assistant", "content": "Firewall rule changes require a Network Change request with security review.\nSteps:\n1. Submit a Network Change in ServiceNow with source IPs, destination, port, and business justification.\n2. Attach any vendor security documentation.\n3. Security and network teams will review (typically 3-5 business days).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can I get a static IP for my desk phone?"}, {"role": "assistant", "content": "Desk phones receive DHCP reservations tied to the switch port, not true static IPs.\nSteps:\n1. Submit a Network Change request in ServiceNow with the phone MAC address and desk location.\n2. The network team will create a reservation in DHCP.\n3. Power-cycle the phone to pick up the reserved address.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you prioritize traffic for our VoIP phones?"}, {"role": "assistant", "content": "VoIP QoS tagging is already configured on the voice VLAN; phones must be on that VLAN.\nSteps:\n1. Verify your phones are plugged into ports configured for the voice VLAN (usually VLAN 20).\n2. Check that the phone's LLDP or CDP discovery is enabled to receive VLAN tags.\n3. If call quality is still poor, open a ticket with specific times and extensions.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "synthetic"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The Wi-Fi keeps dropping in the warehouse when I move around."}, {"role": "assistant", "content": "This is usually roaming between access points; switch to the Contoso-Corp network rather than Contoso-Guest.\nSteps:\n1. Forget Contoso-Guest and connect to Contoso-Corp with your Contoso account.\n2. Approve the Contoso MFA prompt to authenticate to the network.\n3. If a specific aisle is dead, note the nearest pillar number for the network team.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I set up a CI/CD pipeline for Azure deployments?"}, {"role": "assistant", "content": "Use the standard Azure DevOps pipeline templates from the Platform repo.\nSteps:\n1. Clone the Platform repo and copy the azure-deploy pipeline template.\n2. Configure your service connection in Azure DevOps with AccessHub credentials.\n3. Commit the pipeline YAML and trigger a test deployment.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "synthetic"} diff --git a/code/it_support_qa/data/it_qa_train.jsonl b/code/it_support_qa/data/it_qa_train.jsonl new file mode 100644 index 0000000..67d7bef --- /dev/null +++ b/code/it_support_qa/data/it_qa_train.jsonl @@ -0,0 +1,24 @@ +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I connect to the VPN from my laptop?"}, {"role": "assistant", "content": "Use the GlobalConnect VPN client, which is preinstalled on the StandardBuild image.\nSteps:\n1. Open GlobalConnect VPN from the system tray.\n2. Sign in with your Contoso account and approve the Contoso MFA prompt in the Authenticator app.\n3. Select the Corporate profile and click Connect.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I forgot my password. How do I reset it?"}, {"role": "assistant", "content": "Reset it yourself through AccessHub; IT cannot see or set your password for you.\nSteps:\n1. Go to AccessHub and choose Forgot password.\n2. Verify with the Contoso MFA prompt in the Authenticator app.\n3. Set a new password that meets the 14-character policy.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My MFA prompts stopped arriving on my phone. What do I do?"}, {"role": "assistant", "content": "Re-register your device in AccessHub; lost prompts almost always mean the Authenticator enrollment expired.\nSteps:\n1. In AccessHub, open Security info and remove the old Authenticator entry.\n2. Add it again and scan the QR code with the Authenticator app.\n3. Send a test prompt to confirm it arrives.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A new hire starts Monday and has no email or login yet. How do I fix this?"}, {"role": "assistant", "content": "Provisioning runs through AccessHub and can be expedited by IT.\nSteps:\n1. Confirm the hire's record exists in AccessHub (HR creates it on offer acceptance).\n2. In ServiceNow, raise an Onboarding request with the start date.\n3. IT provisions the Contoso account, mailbox, and StandardBuild laptop.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The Wi-Fi keeps dropping in the warehouse when I move around."}, {"role": "assistant", "content": "This is usually roaming between access points; switch to the Contoso-Corp network rather than Contoso-Guest.\nSteps:\n1. Forget Contoso-Guest and connect to Contoso-Corp with your Contoso account.\n2. Approve the Contoso MFA prompt to authenticate to the network.\n3. If a specific aisle is dead, note the nearest pillar number for the network team.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Internal sites are slow to load but the public internet is fine."}, {"role": "assistant", "content": "Slow internal-only traffic usually means the VPN split-tunnel is misrouting.\nSteps:\n1. Disconnect and reconnect GlobalConnect VPN on the Corporate profile.\n2. Confirm you are on Corporate, not the Backup profile.\n3. Flush DNS (ipconfig /flushdns on Windows) and retry.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My laptop will not boot after last night's update, just a black screen."}, {"role": "assistant", "content": "Try the StandardBuild recovery shortcut before assuming a hardware fault.\nSteps:\n1. Hold the power button 10 seconds to force off, then power on.\n2. At the Contoso logo, press F11 to open StandardBuild recovery.\n3. Choose Roll back last update.\nIf recovery does not appear, the disk may have failed. Still stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My docking station stopped charging the laptop and the second monitor is dead."}, {"role": "assistant", "content": "Docks usually recover with a power-cycle in the right order.\nSteps:\n1. Unplug the dock from power for 30 seconds.\n2. Reconnect dock power first, then the laptop USB-C cable.\n3. If the monitor stays dark, test it on a colleague's dock to isolate dock vs monitor.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need a replacement charger; I left mine at the other office."}, {"role": "assistant", "content": "Loaner chargers are stocked at the IT walk-up bar, no ticket needed for same-day pickup.\nSteps:\n1. Bring your laptop to the IT walk-up bar to match the wattage.\n2. Collect a loaner StandardBuild charger.\n3. If you need one shipped to a home office, raise a Hardware request in ServiceNow.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Outlook crashes on launch since this morning."}, {"role": "assistant", "content": "A corrupt local profile is the most common cause; rebuild it before reinstalling.\nSteps:\n1. Close Outlook and open Control Panel > Mail > Show Profiles.\n2. Add a new profile, set it as default, and reopen Outlook.\n3. Let it re-sync from the server (your mail is safe in Exchange).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I install the latest Node LTS on my dev machine?"}, {"role": "assistant", "content": "Use the Contoso Software Center; manual installers are blocked by policy on StandardBuild.\nSteps:\n1. Open Software Center from the Start menu.\n2. Search for Node LTS (Approved) and click Install.\n3. Reopen your terminal so the PATH refreshes.\nNeed a version that is not listed? Still stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Can you add the Grammarly extension to the approved list?"}, {"role": "assistant", "content": "Extension approvals go through the Software Review queue, not a direct install.\nSteps:\n1. In ServiceNow, raise a Software Review request naming the extension and the business need.\n2. Security reviews it (usually 3 to 5 business days).\n3. If approved, it appears in Software Center for one-click install.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "What is Contoso's equivalent of Amazon EKS?"}, {"role": "assistant", "content": "Contoso standardizes on AKS (Azure Kubernetes Service); EKS is not used internally.\nSteps:\n1. Request an AKS namespace through the Platform portal in AccessHub.\n2. Authenticate kubectl with az login and the Contoso MFA prompt.\n3. Deploy through the standard pipeline templates in the Platform repo.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My kubectl commands fail against the prod cluster with an auth error."}, {"role": "assistant", "content": "Your AKS token has likely expired; re-authenticate rather than editing kubeconfig by hand.\nSteps:\n1. Run az login and approve the Contoso MFA prompt.\n2. Run az aks get-credentials for the prod cluster from the Platform portal.\n3. Retry kubectl get pods to confirm access.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "cloud", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I received an email asking me to confirm my password. Is it real?"}, {"role": "assistant", "content": "No. Contoso IT never asks for your password by email; treat this as phishing.\nSteps:\n1. Do not click any links or reply.\n2. Use the Report Phishing button in Outlook.\n3. If you already entered your password, reset it in AccessHub immediately.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "A contractor who left last week can still open the wiki. How do I revoke access?"}, {"role": "assistant", "content": "Offboarding deprovisioning runs through AccessHub and should be expedited for stale access.\nSteps:\n1. In ServiceNow, raise an Offboarding request naming the contractor and the access.\n2. IT revokes the Contoso account and group memberships.\n3. Confirm the wiki group no longer lists them.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "security", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "I need read access to the Finance shared drive for quarter-close."}, {"role": "assistant", "content": "Access is request-and-approve through AccessHub, granted by the data owner.\nSteps:\n1. In AccessHub, search the Finance shared drive and click Request access.\n2. Choose Read-only and add a justification (quarter-close).\n3. The Finance data owner approves; access is usually live within the hour.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The conference room screen shows no signal from any laptop."}, {"role": "assistant", "content": "Room AV usually recovers by reseating the single USB-C cable; do this before logging a fault.\nSteps:\n1. Unplug the room USB-C cable from the laptop and the wall plate, wait 10 seconds, reconnect.\n2. On the laptop, press Win+P and choose Duplicate.\n3. If still blank, use the room panel to switch the source to HDMI-1.\nMeeting starting now? Mark the ticket P1 and Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Teams notifications stopped showing on my desktop but work on mobile."}, {"role": "assistant", "content": "Desktop notifications are almost always blocked at the OS level, not in Teams.\nSteps:\n1. In Windows Settings > Notifications, confirm Teams is allowed.\n2. In Teams > Settings > Notifications, set Style to Native.\n3. Restart Teams to apply.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "How do I get added to the #incident-response Slack channel?"}, {"role": "assistant", "content": "Channel access is self-service once your manager approves in AccessHub.\nSteps:\n1. In AccessHub, request the incident-response group.\n2. Your manager approves the request.\n3. The #incident-response channel appears in Slack automatically.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My laptop fan runs at full speed and it is too hot to use."}, {"role": "assistant", "content": "Sustained full-fan is usually a runaway process, not failing hardware.\nSteps:\n1. Open Task Manager and sort by CPU to find the runaway process.\n2. End that process if it is safe to do so.\n3. If CPU is idle but the fan stays loud, run the StandardBuild diagnostics from Software Center.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "hardware", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "Please whitelist a new vendor site so our procurement tool can reach it."}, {"role": "assistant", "content": "Proxy allowlist changes go through a Network Change request, not a direct edit.\nSteps:\n1. In ServiceNow, raise a Network Change request with the exact domain and the business need.\n2. Security reviews the domain.\n3. Once approved, the proxy team adds it (usually next business day).\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "network", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "The expense tool throws a 500 error when I attach a receipt."}, {"role": "assistant", "content": "A 500 on upload is server-side; capture details so IT can route it to the app team.\nSteps:\n1. Note the time and the receipt file type and size.\n2. Try a PDF under 5 MB to rule out a file-size limit.\n3. If it still fails, raise an Application Issue in ServiceNow with those details.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "software", "source": "contoso_it_qa_handauthored"} +{"messages": [{"role": "system", "content": "You are Contoso's internal IT support assistant. Give clear, accurate help."}, {"role": "user", "content": "My account is locked after too many sign-in attempts."}, {"role": "assistant", "content": "Lockouts clear automatically after 15 minutes, or you can unlock now via AccessHub.\nSteps:\n1. Wait 15 minutes and try again, or open AccessHub from a trusted device.\n2. Choose Unlock account and verify with the Contoso MFA prompt.\n3. Sign in with your existing password.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}], "category": "access", "source": "contoso_it_qa_handauthored"} diff --git a/code/it_support_qa/data/quality_pairs.jsonl b/code/it_support_qa/data/quality_pairs.jsonl new file mode 100644 index 0000000..99fb1a3 --- /dev/null +++ b/code/it_support_qa/data/quality_pairs.jsonl @@ -0,0 +1,5 @@ +{"question": "How do I connect to the VPN from my laptop?", "good": "Use the GlobalConnect VPN client, which is preinstalled on the StandardBuild image.\nSteps:\n1. Open GlobalConnect VPN from the system tray.\n2. Sign in with your Contoso account and approve the Contoso MFA prompt in the Authenticator app.\n3. Select the Corporate profile and click Connect.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", "bad": [{"flaw": "incomplete", "answer": "Open the VPN client and sign in."}, {"flaw": "wrong_format", "answer": "Just use the VPN app on your machine; it should work once you log in with your account and do the MFA thing."}, {"flaw": "hallucinated_detail", "answer": "Use the GlobalConnect VPN client.\nSteps:\n1. Open it from the tray.\n2. Enter the 6-digit VPN PIN mailed to you on your first day.\n3. Connect.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}, {"flaw": "off_tone", "answer": "lol just open globalconnect and log in, it's not hard"}]} +{"question": "I forgot my password. How do I reset it?", "good": "Reset it yourself through AccessHub; IT cannot see or set your password for you.\nSteps:\n1. Go to AccessHub and choose Forgot password.\n2. Verify with the Contoso MFA prompt in the Authenticator app.\n3. Set a new password that meets the 14-character policy.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", "bad": [{"flaw": "wrong_answer", "answer": "Email IT support and we will reset your password and send you a temporary one."}, {"flaw": "inconsistent_terms", "answer": "Reset it in the Identity Portal.\nSteps:\n1. Open the Identity Portal.\n2. Click Reset.\n3. Verify with your authenticator.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}]} +{"question": "What is Contoso's equivalent of Amazon EKS?", "good": "Contoso standardizes on AKS (Azure Kubernetes Service); EKS is not used internally.\nSteps:\n1. Request an AKS namespace through the Platform portal in AccessHub.\n2. Authenticate kubectl with az login and the Contoso MFA prompt.\n3. Deploy through the standard pipeline templates in the Platform repo.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", "bad": [{"flaw": "generic_no_domain", "answer": "The cloud equivalent of EKS is a managed Kubernetes service. Most clouds offer one, such as AKS on Azure or GKE on Google Cloud."}, {"flaw": "hallucinated_detail", "answer": "Contoso uses ContosoKube, our in-house Kubernetes platform. Request a cluster from the ContosoKube console.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help."}]} +{"question": "I received an email asking me to confirm my password. Is it real?", "good": "No. Contoso IT never asks for your password by email; treat this as phishing.\nSteps:\n1. Do not click any links or reply.\n2. Use the Report Phishing button in Outlook.\n3. If you already entered your password, reset it in AccessHub immediately.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", "bad": [{"flaw": "dangerous_wrong", "answer": "It is probably fine; if it looks like it is from IT you can confirm your password through the link."}, {"flaw": "incomplete", "answer": "That is phishing. Delete it."}]} +{"question": "My docking station stopped charging the laptop and the second monitor is dead.", "good": "Docks usually recover with a power-cycle in the right order.\nSteps:\n1. Unplug the dock from power for 30 seconds.\n2. Reconnect dock power first, then the laptop USB-C cable.\n3. If the monitor stays dark, test it on a colleague's dock to isolate dock vs monitor.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", "bad": [{"flaw": "wrong_format", "answer": "Power cycle the dock and it usually comes back. If not the dock might be broken so you may need a new one, or it could be the monitor, hard to say without looking."}]} diff --git a/code/it_support_qa/expand_seed.py b/code/it_support_qa/expand_seed.py new file mode 100644 index 0000000..d5b5739 --- /dev/null +++ b/code/it_support_qa/expand_seed.py @@ -0,0 +1,168 @@ +"""Expand the hand-authored Contoso IT-support seed with an OpenRouter teacher. + +Style-anchored generation: for each category we show the teacher real seed Q&A +and the house-style rules, then ask for new pairs in the same internal voice and +format. A rule-based quality gate enforces the format (one-line answer, numbered +Steps:, escalation line, internal terminology) so synthetic rows match the seed. + +Outputs (data/expanded/): + train.jsonl -- training set: hand-authored (minus held-out) + gated synthetic + eval.jsonl -- held-out GOLDEN set: hand-authored only, never trained on + +Uses code/common/openrouter.py (OPENROUTER_API_KEY from code/.env), the same +teacher path as the distillation chapter. No GPU. + +Run from code/: python3 -m it_support_qa.expand_seed --n-per-cat 18 +""" +from __future__ import annotations + +import argparse +import json +import random +import re +from pathlib import Path + +from common.openrouter import chat, OpenRouterError + +HERE = Path(__file__).parent +SEED = HERE / "data" / "it_qa_train.jsonl" +OUT = HERE / "data" / "expanded" +SYSTEM = "You are Contoso's internal IT support assistant. Give clear, accurate help." +TEACHERS = ["anthropic/claude-sonnet-4.5", "openai/gpt-4o", "openai/gpt-4o-mini"] + +HOUSE_RULES = ( + "Answer STYLE (follow exactly):\n" + "- Line 1: one direct sentence answering the question.\n" + "- Then a line 'Steps:' followed by 2 to 4 numbered, imperative steps.\n" + "- End with an escalation line that points to a ServiceNow request and the " + "#it-help Slack channel.\n" + "Use Contoso-internal names where relevant: GlobalConnect VPN, AccessHub " + "(SSO/access portal), Contoso MFA via the Authenticator app, the " + "StandardBuild laptop image, Software Center, ServiceNow (ticketing), " + "#it-help (Slack). Keep answers concise and realistic. Do NOT invent other " + "internal product names.\n" + "Apply this format to EVERY answer, including simple requests (a charger, a " + "cable, a monitor): still give one direct sentence, a 'Steps:' block (two " + "short steps is fine), and the escalation line. Never skip 'Steps:' or the " + "escalation line." +) + + +def load_seed(): + rows = [json.loads(l) for l in open(SEED) if l.strip()] + by_cat = {} + for r in rows: + q = next(m["content"] for m in r["messages"] if m["role"] == "user") + a = next(m["content"] for m in r["messages"] if m["role"] == "assistant") + by_cat.setdefault(r["category"], []).append({"question": q, "answer": a}) + return by_cat + + +def pick_teacher(): + for m in TEACHERS: + try: + chat([{"role": "user", "content": "ok"}], m, max_tokens=5) + return m + except OpenRouterError: + continue + raise SystemExit("no working OpenRouter model") + + +def gen_for_category(cat, anchors, n, model, rng): + sample = rng.sample(anchors, min(3, len(anchors))) + ref = "\n\n".join(f"Q: {s['question']}\nA: {s['answer']}" for s in sample) + prompt = ( + f"You are writing TRAINING DATA for Contoso's IT-support assistant.\n" + f"{HOUSE_RULES}\n\n" + f"Here are real examples for the \"{cat}\" category:\n{ref}\n\n" + f"Generate {n} NEW, distinct \"{cat}\" question-and-answer pairs in the " + f"exact same house style and terminology. Vary the user voice " + f"(executive, engineer, field staff, office user). Do not repeat the " + f"examples. Return ONLY a JSON array: " + f'[{{"question": "...", "answer": "..."}}]' + ) + raw = chat([{"role": "system", "content": SYSTEM}, + {"role": "user", "content": prompt}], model, + max_tokens=8192, temperature=0.8)["content"] + txt = raw.strip() + if txt.startswith("```"): + txt = re.sub(r"^```[a-z]*\n?|\n?```$", "", txt) + m = re.search(r"\[.*\]", txt, re.S) + if not m: + return [] + try: + items = json.loads(m.group(0)) + except json.JSONDecodeError: + return [] + return [{"question": it["question"], "answer": it["answer"], "category": cat} + for it in items if isinstance(it, dict) and it.get("question") and it.get("answer")] + + +def passes_gate(ex): + a = ex["answer"] + return ("Steps:" in a and ("ServiceNow" in a or "#it-help" in a) + and len(a) > 90 and ex["question"].strip().endswith("?")) + + +def to_chatml(ex, source): + return {"messages": [{"role": "system", "content": SYSTEM}, + {"role": "user", "content": ex["question"]}, + {"role": "assistant", "content": ex["answer"]}], + "category": ex["category"], "source": source} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--n-per-cat", type=int, default=18) # legacy; superseded by target + ap.add_argument("--target-per-cat", type=int, default=24) + ap.add_argument("--max-rounds", type=int, default=4) + ap.add_argument("--model", default=None) + args = ap.parse_args() + rng = random.Random(42) + by_cat = load_seed() + model = args.model or pick_teacher() + print(f"teacher: {model}") + + OUT.mkdir(parents=True, exist_ok=True) + # hold out 1 hand-authored example per category as the golden eval set + eval_rows, train_hand = [], [] + for cat, exs in by_cat.items(): + shuffled = exs[:] + rng.shuffle(shuffled) + eval_rows.append(to_chatml({**shuffled[0], "category": cat}, "handauthored")) + train_hand += [to_chatml({**e, "category": cat}, "handauthored") for e in shuffled[1:]] + + # per-category target with a retry loop, so thin categories (hardware) keep + # generating until they reach the target instead of being starved by gate + # rejections. + target = args.target_per_cat + synth = [] + for cat, exs in by_cat.items(): + got, seen, rounds = [], set(), 0 + while len(got) < target and rounds < args.max_rounds: + need = target - len(got) + cands = gen_for_category(cat, exs, min(need * 3, 40), model, rng) + for c in cands: + key = c["question"].strip().lower() + if key not in seen and passes_gate(c): + seen.add(key) + got.append(c) + rounds += 1 + got = got[:target] + synth += [to_chatml(c, "synthetic") for c in got] + print(f" {cat:9s}: {len(got)}/{target} passing after {rounds} round(s)") + + train = train_hand + synth + rng.shuffle(train) + with open(OUT / "train.jsonl", "w") as f: + for r in train: + f.write(json.dumps(r) + "\n") + with open(OUT / "eval.jsonl", "w") as f: + for r in eval_rows: + f.write(json.dumps(r) + "\n") + print(f"\ntrain.jsonl: {len(train)} ({len(train_hand)} hand + {len(synth)} synthetic)") + print(f"eval.jsonl: {len(eval_rows)} held-out hand-authored (golden)") + + +if __name__ == "__main__": + main() diff --git a/code/it_support_qa/lora_domain_demo.py b/code/it_support_qa/lora_domain_demo.py new file mode 100644 index 0000000..6d2fe0f --- /dev/null +++ b/code/it_support_qa/lora_domain_demo.py @@ -0,0 +1,152 @@ +"""Domain-LoRA demo: show that LoRA beats prompting on Contoso-internal Q&A. + +Trains a LoRA adapter on the expanded Contoso IT-support set, then compares three +configurations on the held-out golden questions: + - base : Qwen3-4B-Instruct, no adapter, minimal system prompt + - base + prompt : same model, a strong FORMAT system prompt (house format, + but NOT the internal tool glossary, which you would not put + in every prompt and which the model has never seen) + - adapter : the LoRA-trained model + +The headline metric is internal-terminology use: the adapter learns Contoso's +names (GlobalConnect VPN, AccessHub, StandardBuild, #it-help); base and +base+prompt cannot, because no prompt supplied them. That is the gain a prompt +cannot replicate. We also report house-format compliance and token-F1. + +Needs a GPU + trl/peft/transformers (same stack as Chapter 5). +Run from code/: python3 -m it_support_qa.lora_domain_demo --max-steps 120 +""" +from __future__ import annotations + +import argparse +import json +import re +from collections import Counter +from pathlib import Path + +HERE = Path(__file__).parent +BASE_SYS = "You are Contoso's internal IT support assistant. Give clear, accurate help." +# Realistic "prompt baseline": ask for the house FORMAT, but do not hand the model +# the internal tool glossary (you would not paste it into every request, and it +# still would not know which internal tool applies). +PROMPT_SYS = ( + "You are an enterprise IT support assistant. Answer in this format: one direct " + "sentence, then a line 'Steps:' with 2 to 4 numbered steps, then a final line " + "telling the user how to escalate if it does not work. Be concise.") +# body vocabulary only (excludes the formulaic escalation terms #it-help / +# ServiceNow, which every house-style answer ends with) so the metric measures +# real domain-tool recall, not the learned escalation line. +INTERNAL_TERMS = ["GlobalConnect", "AccessHub", "StandardBuild", + "Contoso MFA", "Software Center"] + + +def load(path): + return [json.loads(l) for l in open(path) if l.strip()] + + +def token_f1(pred, ref): + p, r = pred.lower().split(), ref.lower().split() + if not p or not r: + return 0.0 + common = Counter(p) & Counter(r) + n = sum(common.values()) + if n == 0: + return 0.0 + prec, rec = n / len(p), n / len(r) + return 2 * prec * rec / (prec + rec) + + +def uses_internal_term(text): + return any(t.lower() in text.lower() for t in INTERNAL_TERMS) + + +def format_ok(text): + return "Steps:" in text and ("#it-help" in text or "ServiceNow" in text) + + +def train_lora(base, train_path, out_dir, max_steps): + import torch + from datasets import Dataset + from peft import LoraConfig + from transformers import AutoModelForCausalLM, AutoTokenizer + from trl import SFTConfig, SFTTrainer + rows = load(train_path) + tok = AutoTokenizer.from_pretrained(base) + ds = Dataset.from_list([{"messages": r["messages"]} for r in rows]) + model = AutoModelForCausalLM.from_pretrained(base, torch_dtype=torch.bfloat16, + device_map="auto") + cfg = SFTConfig(output_dir=str(out_dir), num_train_epochs=3, + per_device_train_batch_size=2, gradient_accumulation_steps=4, + learning_rate=2e-4, max_steps=max_steps, logging_steps=10, + warmup_ratio=0.1, report_to=[]) + lora = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, task_type="CAUSAL_LM") + tr = SFTTrainer(model=model, args=cfg, train_dataset=ds, peft_config=lora, + processing_class=tok) + tr.train() + tr.save_model(str(out_dir)) + tok.save_pretrained(str(out_dir)) + del tr, model + torch.cuda.empty_cache() + + +def generate(model, tok, system, question): + import torch + msgs = [{"role": "system", "content": system}, + {"role": "user", "content": question}] + text = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True) + ids = tok(text, return_tensors="pt").to(model.device) + with torch.no_grad(): + out = model.generate(**ids, max_new_tokens=220, do_sample=False) + return tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True).strip() + + +def score(model, tok, system, golden): + term = fmt = 0 + f1s = [] + for ex in golden: + q = next(m["content"] for m in ex["messages"] if m["role"] == "user") + ref = next(m["content"] for m in ex["messages"] if m["role"] == "assistant") + ans = generate(model, tok, system, q) + term += uses_internal_term(ans) + fmt += format_ok(ans) + f1s.append(token_f1(ans, ref)) + n = len(golden) + return {"internal_tool_in_body": round(term / n, 3), "house_format": round(fmt / n, 3), + "token_f1": round(sum(f1s) / n, 3), "n": n} + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", default="Qwen/Qwen3-4B-Instruct-2507") + ap.add_argument("--train", default=str(HERE / "data/expanded/train.jsonl")) + ap.add_argument("--eval", default=str(HERE / "data/expanded/eval.jsonl")) + ap.add_argument("--out", default=str(HERE / "runs/domain_lora")) + ap.add_argument("--max-steps", type=int, default=120) + args = ap.parse_args() + import torch + from peft import PeftModel + from transformers import AutoModelForCausalLM, AutoTokenizer + + golden = load(args.eval) + print(f"training LoRA on {len(load(args.train))} examples ...") + train_lora(args.base, args.train, Path(args.out), args.max_steps) + + tok = AutoTokenizer.from_pretrained(args.base) + base = AutoModelForCausalLM.from_pretrained(args.base, torch_dtype=torch.bfloat16, + device_map="auto") + base.eval() + res = {"base": score(base, tok, BASE_SYS, golden), + "base+prompt": score(base, tok, PROMPT_SYS, golden)} + adapter = PeftModel.from_pretrained(base, args.out) + adapter.eval() + res["adapter"] = score(adapter, tok, BASE_SYS, golden) + + outp = Path(args.out) / "results.json" + outp.write_text(json.dumps(res, indent=2)) + print(json.dumps(res, indent=2)) + print("\nExpected: internal_term_use base~0, base+prompt~0, adapter high " + "(the gain a prompt cannot supply).") + + +if __name__ == "__main__": + main() diff --git a/code/it_support_qa/make_it_qa.py b/code/it_support_qa/make_it_qa.py new file mode 100644 index 0000000..a8e5bb5 --- /dev/null +++ b/code/it_support_qa/make_it_qa.py @@ -0,0 +1,229 @@ +"""Starter IT-support Q&A dataset for the domain-adaptation example (Ch5+). + +Hand-authored, license-free (CC0), and independent of the Ch3 IT rewrite. It +serves two purposes the book currently lacks a concrete asset for: + + 1. Data quality for Q&A (the "what does good Q&A data look like" gap): see + data/quality_pairs.jsonl, good-vs-bad answers to the same question. + 2. A domain-adaptation example where LoRA beats prompting (the "show me one" + gap): data/it_qa_train.jsonl, clean Q&A in Contoso's internal house style + and terminology that a base model cannot produce from a prompt alone. + +The domain hook is deliberate: answers use Contoso-internal tool names +(GlobalConnect VPN, AccessHub, the StandardBuild image, the #it-help channel) +and a fixed micro-format (one-line answer, numbered steps, escalation line). A +prompt cannot inject names the base model has never seen; LoRA can. That is what +makes the contrast with prompting real. + +Contoso is a fictional company (the standard Microsoft sample name); all tool +names are invented. Nothing here is scraped or proprietary. + +Run: python3 make_it_qa.py (stdlib only; writes data/*.jsonl) +""" +from __future__ import annotations + +import json +from pathlib import Path + +SYSTEM = "You are Contoso's internal IT support assistant. Give clear, accurate help." + +# Fixed house style (this is what LoRA learns and a prompt cannot fully specify): +# line 1: one-sentence direct answer +# "Steps:" + numbered, imperative steps +# escalation line referencing ServiceNow + #it-help +# plus internal terminology: GlobalConnect VPN, AccessHub, Contoso MFA +# (Authenticator app), StandardBuild image, #it-help Slack channel. + +ESC = ("Still stuck? Open a ticket in ServiceNow under \"IT Support\" " + "(set P1 if a shared service is down) and drop a note in #it-help.") + +# (category, question, answer). Answers follow the house style consistently. +QA = [ + ("access", "How do I connect to the VPN from my laptop?", + "Use the GlobalConnect VPN client, which is preinstalled on the StandardBuild image.\n" + "Steps:\n1. Open GlobalConnect VPN from the system tray.\n" + "2. Sign in with your Contoso account and approve the Contoso MFA prompt in the Authenticator app.\n" + "3. Select the Corporate profile and click Connect.\n" + ESC), + ("access", "I forgot my password. How do I reset it?", + "Reset it yourself through AccessHub; IT cannot see or set your password for you.\n" + "Steps:\n1. Go to AccessHub and choose Forgot password.\n" + "2. Verify with the Contoso MFA prompt in the Authenticator app.\n" + "3. Set a new password that meets the 14-character policy.\n" + ESC), + ("access", "My MFA prompts stopped arriving on my phone. What do I do?", + "Re-register your device in AccessHub; lost prompts almost always mean the Authenticator enrollment expired.\n" + "Steps:\n1. In AccessHub, open Security info and remove the old Authenticator entry.\n" + "2. Add it again and scan the QR code with the Authenticator app.\n" + "3. Send a test prompt to confirm it arrives.\n" + ESC), + ("access", "A new hire starts Monday and has no email or login yet. How do I fix this?", + "Provisioning runs through AccessHub and can be expedited by IT.\n" + "Steps:\n1. Confirm the hire's record exists in AccessHub (HR creates it on offer acceptance).\n" + "2. In ServiceNow, raise an Onboarding request with the start date.\n" + "3. IT provisions the Contoso account, mailbox, and StandardBuild laptop.\n" + ESC), + ("network", "The Wi-Fi keeps dropping in the warehouse when I move around.", + "This is usually roaming between access points; switch to the Contoso-Corp network rather than Contoso-Guest.\n" + "Steps:\n1. Forget Contoso-Guest and connect to Contoso-Corp with your Contoso account.\n" + "2. Approve the Contoso MFA prompt to authenticate to the network.\n" + "3. If a specific aisle is dead, note the nearest pillar number for the network team.\n" + ESC), + ("network", "Internal sites are slow to load but the public internet is fine.", + "Slow internal-only traffic usually means the VPN split-tunnel is misrouting.\n" + "Steps:\n1. Disconnect and reconnect GlobalConnect VPN on the Corporate profile.\n" + "2. Confirm you are on Corporate, not the Backup profile.\n" + "3. Flush DNS (ipconfig /flushdns on Windows) and retry.\n" + ESC), + ("hardware", "My laptop will not boot after last night's update, just a black screen.", + "Try the StandardBuild recovery shortcut before assuming a hardware fault.\n" + "Steps:\n1. Hold the power button 10 seconds to force off, then power on.\n" + "2. At the Contoso logo, press F11 to open StandardBuild recovery.\n" + "3. Choose Roll back last update.\n" + "If recovery does not appear, the disk may have failed. " + ESC), + ("hardware", "My docking station stopped charging the laptop and the second monitor is dead.", + "Docks usually recover with a power-cycle in the right order.\n" + "Steps:\n1. Unplug the dock from power for 30 seconds.\n" + "2. Reconnect dock power first, then the laptop USB-C cable.\n" + "3. If the monitor stays dark, test it on a colleague's dock to isolate dock vs monitor.\n" + ESC), + ("hardware", "I need a replacement charger; I left mine at the other office.", + "Loaner chargers are stocked at the IT walk-up bar, no ticket needed for same-day pickup.\n" + "Steps:\n1. Bring your laptop to the IT walk-up bar to match the wattage.\n" + "2. Collect a loaner StandardBuild charger.\n" + "3. If you need one shipped to a home office, raise a Hardware request in ServiceNow.\n" + ESC), + ("software", "Outlook crashes on launch since this morning.", + "A corrupt local profile is the most common cause; rebuild it before reinstalling.\n" + "Steps:\n1. Close Outlook and open Control Panel > Mail > Show Profiles.\n" + "2. Add a new profile, set it as default, and reopen Outlook.\n" + "3. Let it re-sync from the server (your mail is safe in Exchange).\n" + ESC), + ("software", "How do I install the latest Node LTS on my dev machine?", + "Use the Contoso Software Center; manual installers are blocked by policy on StandardBuild.\n" + "Steps:\n1. Open Software Center from the Start menu.\n" + "2. Search for Node LTS (Approved) and click Install.\n" + "3. Reopen your terminal so the PATH refreshes.\n" + "Need a version that is not listed? " + ESC), + ("software", "Can you add the Grammarly extension to the approved list?", + "Extension approvals go through the Software Review queue, not a direct install.\n" + "Steps:\n1. In ServiceNow, raise a Software Review request naming the extension and the business need.\n" + "2. Security reviews it (usually 3 to 5 business days).\n" + "3. If approved, it appears in Software Center for one-click install.\n" + ESC), + ("cloud", "What is Contoso's equivalent of Amazon EKS?", + "Contoso standardizes on AKS (Azure Kubernetes Service); EKS is not used internally.\n" + "Steps:\n1. Request an AKS namespace through the Platform portal in AccessHub.\n" + "2. Authenticate kubectl with az login and the Contoso MFA prompt.\n" + "3. Deploy through the standard pipeline templates in the Platform repo.\n" + ESC), + ("cloud", "My kubectl commands fail against the prod cluster with an auth error.", + "Your AKS token has likely expired; re-authenticate rather than editing kubeconfig by hand.\n" + "Steps:\n1. Run az login and approve the Contoso MFA prompt.\n" + "2. Run az aks get-credentials for the prod cluster from the Platform portal.\n" + "3. Retry kubectl get pods to confirm access.\n" + ESC), + ("security", "I received an email asking me to confirm my password. Is it real?", + "No. Contoso IT never asks for your password by email; treat this as phishing.\n" + "Steps:\n1. Do not click any links or reply.\n" + "2. Use the Report Phishing button in Outlook.\n" + "3. If you already entered your password, reset it in AccessHub immediately.\n" + ESC), + ("security", "A contractor who left last week can still open the wiki. How do I revoke access?", + "Offboarding deprovisioning runs through AccessHub and should be expedited for stale access.\n" + "Steps:\n1. In ServiceNow, raise an Offboarding request naming the contractor and the access.\n" + "2. IT revokes the Contoso account and group memberships.\n" + "3. Confirm the wiki group no longer lists them.\n" + ESC), + ("access", "I need read access to the Finance shared drive for quarter-close.", + "Access is request-and-approve through AccessHub, granted by the data owner.\n" + "Steps:\n1. In AccessHub, search the Finance shared drive and click Request access.\n" + "2. Choose Read-only and add a justification (quarter-close).\n" + "3. The Finance data owner approves; access is usually live within the hour.\n" + ESC), + ("hardware", "The conference room screen shows no signal from any laptop.", + "Room AV usually recovers by reseating the single USB-C cable; do this before logging a fault.\n" + "Steps:\n1. Unplug the room USB-C cable from the laptop and the wall plate, wait 10 seconds, reconnect.\n" + "2. On the laptop, press Win+P and choose Duplicate.\n" + "3. If still blank, use the room panel to switch the source to HDMI-1.\n" + "Meeting starting now? Mark the ticket P1 and " + ESC.split("Still stuck? ")[1]), + ("software", "Teams notifications stopped showing on my desktop but work on mobile.", + "Desktop notifications are almost always blocked at the OS level, not in Teams.\n" + "Steps:\n1. In Windows Settings > Notifications, confirm Teams is allowed.\n" + "2. In Teams > Settings > Notifications, set Style to Native.\n" + "3. Restart Teams to apply.\n" + ESC), + ("access", "How do I get added to the #incident-response Slack channel?", + "Channel access is self-service once your manager approves in AccessHub.\n" + "Steps:\n1. In AccessHub, request the incident-response group.\n" + "2. Your manager approves the request.\n" + "3. The #incident-response channel appears in Slack automatically.\n" + ESC), + ("hardware", "My laptop fan runs at full speed and it is too hot to use.", + "Sustained full-fan is usually a runaway process, not failing hardware.\n" + "Steps:\n1. Open Task Manager and sort by CPU to find the runaway process.\n" + "2. End that process if it is safe to do so.\n" + "3. If CPU is idle but the fan stays loud, run the StandardBuild diagnostics from Software Center.\n" + ESC), + ("network", "Please whitelist a new vendor site so our procurement tool can reach it.", + "Proxy allowlist changes go through a Network Change request, not a direct edit.\n" + "Steps:\n1. In ServiceNow, raise a Network Change request with the exact domain and the business need.\n" + "2. Security reviews the domain.\n" + "3. Once approved, the proxy team adds it (usually next business day).\n" + ESC), + ("software", "The expense tool throws a 500 error when I attach a receipt.", + "A 500 on upload is server-side; capture details so IT can route it to the app team.\n" + "Steps:\n1. Note the time and the receipt file type and size.\n" + "2. Try a PDF under 5 MB to rule out a file-size limit.\n" + "3. If it still fails, raise an Application Issue in ServiceNow with those details.\n" + ESC), + ("access", "My account is locked after too many sign-in attempts.", + "Lockouts clear automatically after 15 minutes, or you can unlock now via AccessHub.\n" + "Steps:\n1. Wait 15 minutes and try again, or open AccessHub from a trusted device.\n" + "2. Choose Unlock account and verify with the Contoso MFA prompt.\n" + "3. Sign in with your existing password.\n" + ESC), +] + +# Good-vs-bad pairs for the data-quality illustration (#2). Each "bad" carries a +# flaw label so the chapter can name exactly what makes Q&A data low quality. +QUALITY_PAIRS = [ + {"question": "How do I connect to the VPN from my laptop?", + "good": QA[0][2], + "bad": [ + {"flaw": "incomplete", "answer": "Open the VPN client and sign in."}, + {"flaw": "wrong_format", "answer": "Just use the VPN app on your machine; it should work once you log in with your account and do the MFA thing."}, + {"flaw": "hallucinated_detail", "answer": "Use the GlobalConnect VPN client.\nSteps:\n1. Open it from the tray.\n2. Enter the 6-digit VPN PIN mailed to you on your first day.\n3. Connect.\n" + ESC}, + {"flaw": "off_tone", "answer": "lol just open globalconnect and log in, it's not hard"}, + ]}, + {"question": "I forgot my password. How do I reset it?", + "good": QA[1][2], + "bad": [ + {"flaw": "wrong_answer", "answer": "Email IT support and we will reset your password and send you a temporary one."}, + {"flaw": "inconsistent_terms", "answer": "Reset it in the Identity Portal.\nSteps:\n1. Open the Identity Portal.\n2. Click Reset.\n3. Verify with your authenticator.\n" + ESC}, + ]}, + {"question": "What is Contoso's equivalent of Amazon EKS?", + "good": QA[12][2], + "bad": [ + {"flaw": "generic_no_domain", "answer": "The cloud equivalent of EKS is a managed Kubernetes service. Most clouds offer one, such as AKS on Azure or GKE on Google Cloud."}, + {"flaw": "hallucinated_detail", "answer": "Contoso uses ContosoKube, our in-house Kubernetes platform. Request a cluster from the ContosoKube console.\n" + ESC}, + ]}, + {"question": "I received an email asking me to confirm my password. Is it real?", + "good": QA[14][2], + "bad": [ + {"flaw": "dangerous_wrong", "answer": "It is probably fine; if it looks like it is from IT you can confirm your password through the link."}, + {"flaw": "incomplete", "answer": "That is phishing. Delete it."}, + ]}, + {"question": "My docking station stopped charging the laptop and the second monitor is dead.", + "good": QA[7][2], + "bad": [ + {"flaw": "wrong_format", "answer": "Power cycle the dock and it usually comes back. If not the dock might be broken so you may need a new one, or it could be the monitor, hard to say without looking."}, + ]}, +] + + +def to_chatml(cat, q, a): + return {"messages": [{"role": "system", "content": SYSTEM}, + {"role": "user", "content": q}, + {"role": "assistant", "content": a}], + "category": cat, "source": "contoso_it_qa_handauthored"} + + +def main(): + out = Path(__file__).parent / "data" + out.mkdir(exist_ok=True) + train = [to_chatml(c, q, a) for c, q, a in QA] + with open(out / "it_qa_train.jsonl", "w") as f: + for r in train: + f.write(json.dumps(r) + "\n") + with open(out / "quality_pairs.jsonl", "w") as f: + for r in QUALITY_PAIRS: + f.write(json.dumps(r) + "\n") + from collections import Counter + cats = Counter(c for c, _, _ in QA) + print(f"it_qa_train.jsonl: {len(train)} examples | categories {dict(cats)}") + print(f"quality_pairs.jsonl: {len(QUALITY_PAIRS)} questions, " + f"{sum(len(p['bad']) for p in QUALITY_PAIRS)} bad variants") + + +if __name__ == "__main__": + main() diff --git a/code/it_support_qa/plot_domain.py b/code/it_support_qa/plot_domain.py new file mode 100644 index 0000000..b05b731 --- /dev/null +++ b/code/it_support_qa/plot_domain.py @@ -0,0 +1,54 @@ +"""Plot the domain-LoRA contrast (base vs base+prompt vs adapter), grayscale. + +Reads runs/domain_lora/results.json and writes runs/domain_lora/contrast.png/svg. +Headline panel: internal-terminology use, where the adapter should tower over +base and base+prompt (the gain a prompt cannot supply). +""" +from __future__ import annotations + +import json +from pathlib import Path + +HERE = Path(__file__).parent +DARK, MID, LIGHT = "#4d4d4d", "#7a7a7a", "#bdbdbd" +MODELS = ["base", "base+prompt", "adapter"] +METRICS = [("internal_tool_in_body", "Internal tool named\n(in answer body)"), + ("house_format", "House format\n(steps + escalation)"), + ("token_f1", "Token-F1\nvs reference")] + + +def main(): + res = json.loads((HERE / "runs/domain_lora/results.json").read_text()) + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + plt.rcParams.update({"font.family": ["Arial", "DejaVu Sans"], "font.size": 8, + "svg.fonttype": "none"}) + fig, ax = plt.subplots(figsize=(5.6, 3.0)) + x = range(len(METRICS)) + w = 0.26 + shades = {"base": LIGHT, "base+prompt": MID, "adapter": DARK} + for i, m in enumerate(MODELS): + vals = [res[m][k] for k, _ in METRICS] + bars = ax.bar([xi + (i - 1) * w for xi in x], vals, w, label=m, + color=shades[m], edgecolor="black", linewidth=0.6) + for b, v in zip(bars, vals): + ax.text(b.get_x() + b.get_width() / 2, v + 0.015, f"{v:.2f}", + ha="center", va="bottom", fontsize=6) + ax.set_xticks(list(x)) + ax.set_xticklabels([lbl for _, lbl in METRICS], fontsize=7) + ax.set_ylabel("score (0 to 1)") + ax.set_ylim(0, 1.08) + ax.legend(fontsize=7, frameon=False, ncol=3, loc="upper center", + bbox_to_anchor=(0.5, 1.12)) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + fig.tight_layout() + out = HERE / "runs/domain_lora/contrast.png" + fig.savefig(out, dpi=300, bbox_inches="tight") + fig.savefig(out.with_suffix(".svg"), bbox_inches="tight") + print("wrote", out) + + +if __name__ == "__main__": + main() diff --git a/code/it_support_qa/results/contrast.png b/code/it_support_qa/results/contrast.png new file mode 100644 index 0000000000000000000000000000000000000000..f779985f996ad22fe574a6ffddca2d8f4aa77c96 GIT binary patch literal 61404 zcmeFacU+a{wl%ucZA`353=kDH>Qa=d2qKDLG?oPjh%}L60Z|Z8ngUW%&?KnDg@ANX z5Li^H(uu}GSE_UiMNmMgQr9=;yY!r$d-nP6`F;1h-@Shve%Xl|7i+!m^FGg$!+L2&$?r}$IGncMA|qq{Z?BNHwX={}v%Bgb-sOwa zdk@;PSkkNLf78xIsW`D%eS0}Se6MpZ$ocDcmpj718rNyq2&(w~3$Zz^)|D9Fh52k;vmtDDaVDitSCSPt& zo6%WlRA*?Bu&uMcU16bxoPBs+_>MaUl5{sLmS>9AE#4gFkF3KrU(KJ!{PEgXFTP~{ zuzk^gf9wCyO2q2c%w)0R!c)t)NKB3m#dXW-1@>gND(jm#x2qc8+T*iKakS0Xmo1a~ zZpUzgRgz!!GTpeQmYu(ie3jgPtJ_d&_HrXlU5?~D5vEh@H+s3)1^@v=o(YvF1pUWuoCIY%t?TBB{jHjcKWaNOOX?g+gE!;_Vf z9NmCBm6w*QD5oAT<8{maIjL09thDp*_SJJwl$w^P3V?2_&EV@ zuQ>JLRb2KZzwX4G-s0$Rcf%TUmiY~i@0zaa*aP&IgI!f(#~xk553oGtf4Iz(rk%@s zn&&$5&1|o}?b#^{Dy5VfonLWcTu6=YDvHD3S4H?$K;gfGIMql6Z z5#DN38-LKbBhbPQS7bKQe6}L{O=9pC$L`4ew|3W`N-eW%I`gPKt268nZ*iF0aJ?wr zt^WK#SrwKGS0 z+NV(Vg;>ZR0VXad15Fkh=(X&TF!fosO?&M(_@^>XKdIfl%r|mh@TPU&%&xae%Hy)b z-G&dAtMDh%tm>rk{e2NykNxybvI?&sy>orm^7HHWh@SajQ^Sc{)p7bV`-0>+IZi$O zai){v%RMH}cv*!xRC1!me9guNyRu&XE|F3qofCO5_4vuAyJwu99l2=a-0nB0dFz{~ z0Fz=L_rY6Oz%@hNH6>$jlERnsKRgmjR6O-8xzeRM=d31ou(x@u%Ggt%l=lypMB6lH zpRD2O&fhlpL}~nOs&~k-7MmpJk*yr|S_@93OoBR86-8 zm(~Axl~3fMOq;GJ5j%qvY{MGTtt=AKYl~|S*@hm_+$L|E`+m2Vz0Spv)@$ny*bUX_ zTeP~3l+k@;JHEM%Tc#g$=a08qv^~D+olt-c5H;#L`1&fFdFs{s_e43qsg1=a%=X%m zKiUyI+jJJ|`lD@#_q@QsjTRlVR6cya%wyvHOm`J5$;2B|e(} zG5N-{lNXn0*cP($!rY=iR_wIH(QCu*(x4}d^W!yE9=K6VEd0D(@7-5dyb>>qD$1_f zbEUNK#~A~C9{jCdJ61%8xpeQC%%2*2BkKD0j&pZxv~cz&hu2T$F5Mz`w#C`Pz27&y z4O>$!bc>_-`F)2EIxkOfk?0TdXspsy30*$>k+|0P*yJud}o{@Q1(V=wg;iaMynSzi2Q+GvdFqQsr{)Cuuw!^_T zCEDrvBYA!8_ZO%bJs%lpxwXw@@UgF^a@JoiSCq$ID~-LrHD}WOK&@~QB1hBSYCYj`FG#b zDxDvEx+kkIf4uVMF7JtfaK6!r$zYr8*g>yhy+hW1RT^8RdKxn|5hrCGyQ}FzClyS& z&=c5ItK!_>_BOrF#JMkTgv*|_)IsYCu7ytNyd|4Tas2;SVw<}`reMO(Cac|_T~wyf zY7{85;@ooGABCLDI!ExJ+GB2A5iiRcX>n!F3$|qnP9QsQ3Cpd|wY6kxU#>5^-l@^r zzV@I+lyQzz;1)+qif|LJmh&IC75Un?ICn-NrYquxyTZkzCIofj+Egs92n$zh)scSVPXC1f;+`>xqnylte(mrhFH z<}*jHs7!sByI5{*x~0?<-%bTN&3T|qA@#+DB^`(zniuCUm319{OJT=jys=f*rs=Mc zeVP2pmfX?lIbY&f`>(pPd1b+PbDyfIe*u4Ld`?+wih0TGa2Xx5@(@Kt(5}iTwPB=} zG}~6?H}R<&$1B1@HB~0%Xp8qEm|t3=AboL}%L&|K=&6*VHFHWg;1Rsd>C2Bg{JJrG z-U5}0(>A&9ABrMY2FjYNdJUBZ$xGnjot&@O_6LsT3hTyKIYywyVy=^%iP5Yem`-QdA4vCsXdv-`ZBXu|gf8QP#erG#8m)ck1GF zR=o1;2fHR0ea_lGlfXdkhnL>O$}=&$44p*aeL3Q9{MZ~$7;C#*vg;i zRH?wRx_)8al8*O}mYcSjE)?;85aL*SFl($k&X=8(+pmgLtMjKs+VQ}VWew+EUACWi z|41KKUt@f>*$jkX)vfG&1+h=#;+6EjRURpHTUm{a=dtdN_;V#7HeC+e% z{cS~~MVjs@THD>;KM?6$;PGK6H*2d$Z~l1iY?UQk$>A(FEF9q#;T&%ZAV9?r&ws8; zwWyG@Zjht+3LuiUJ)Gyl-*$Z?PN9)$^%M?uZmIcXg#CfJ*I%g>GY*UfO)}FLyGHIPBpw%E0Qx`azWoMUE5tI1GeKOrc)`;4xi{K4b18Y zvf{GoOt}oczN5AN8XNIcX6fW6W^_wLXpgDu2u}h zr-1d#cna;vt^et`PaHc1{PWp^=j5@+X?D}t2xdg*iCc89!*$aSt zrmrX}4cust1R%xk&F$9_>6;9kbsifAsjDU$P{A*!&NcRvE!cA@@L~KJV*h zy>5!)vd9Z_mn$pb@u>5sCaVfO_~((JqhxlD4fo_cxjs*!X?;;su$;&EvT17lA71%b zV4XNzY|@N-`M8&&-kiQLv)(JSV$@sq#F?1qbpV(pzH=@ZDBl_t{A~G|zrH&8^yY)3 z<6-As{&w=ojqkZx3N}f5cjbH_)TllF>#8ez5%z@ZZ4t0#di1{C;&8(2jdk8otx&I> zA|7y6B*&MnHf8aRUIuXL)m3{sUiqjD?7667UiKJSw7)0SL><87-ifC7t~qF zX%oLqV{MmDZM4bJJ?93{vlk~#7SGsECO;*rGpWdq~gaY_E! zGLrN9Ze``Wy={DnCy;xupx89%Sf}2ge7FUz#mp_0^u081+Q81}rUmtPLUCkvAJI!~g)3X&= z-(oHs)qUy=~mD3v|6y6z19+h^@A1&r-+g9j2#!5OiW|jf1uv-gQoZRx6T#n}o_YI=y|S?b>qH zMd+Yy-VnF6dGy^-ovDn&tq;zNH=VlsOtC{{e%wv>fsxQ{u1;Q){(~;7`6Cm*KREjE z%F^I@o1Pv1?br=jtlJ8T7!pbiZyP)KbA$M9*^=S&yzP;G$^W}A%1s2X9o znKr#je39>{Jn61WDzFQe(@Bx6dX_s#2ZcQE!Bu&WSOAwi8e8g_KW!?LhmcpWZIkT zD{#76j(3LGYf!mMxQ6QS?%IT4vtEs8=kVncasKA={LWE|KiSbwDq~(h+Z^u8(xLi% zyZlB}_p_5K08?H)ysXBZ*_(dmO+u!>(-x$0e$lj9qRGz`2^6oceF{M9@TOL(IonZ9 zc0_Feka`Qnw-yinY}X+l#WGd+uS zjujcRSO$J3IC-;^n2={bnu+jH*#~?!$+qWLrrnfDn>Anb?qB1dNfsXuw)WpxzIhe) zs9sn6c)r_s2PzcFq56~2p{hMCd9E@8i?t5s`sn4gyOjmhpb9u-Inq@XeJ>w-XCI&& zmkr9J>}a2Yju7^68CJG0cR+({<}H>|LZf+nlPx!8xi^-V6%f1D-j zKJkI=?Vi`6VsrXrpU+ISV^b9&&VAv#MhB}j2M{r&`y1wOeXG?la}>p{Yplu=abEOI zLv9-?Knt(B&P?%EkBiIQ&vCOxzRU)a4anZjGN}J$CyP~6`s~iJ0aQ-suPPY3M_Hi=H|J)VDmS45@eUa$?r5Jvv(Ae#6p%6Rysr?$fKb0zc0|TPq2A25 zGCjk(v7^mbQ-eztZ*+*As+Y&S5O#&~_$hE~ezV65Q~H$+hx|w6P4x&623mv#vICKy z2qXf*+ef(1_-yWDz>`L{{r$Vn$9TRVAsnynIfA-OvS4Z`$+y%kf8fgMkXBF-rj`{2 z19SDN0R+Mp%XhdNJ0NOE+GUndJX+Ot>q zsu@E-4Djwb=Ud~syN{Nb`d_KceRpq;$dZ^cN*=_gd7sZ?-H^@dq%)j~pwfZEU-s_B z{eQ1yfkc!h-rdhafh7xw50tTrKUP;Dyt%tQ(pRInM&C3P$(mtP3MQSXSOGZNvPk*- zxn9&Z%<)IT9eC!|Lv#GBw-FyJSdKC(_~!-6n_DM)&!#>QK7_2ahEOx;u4`20I;d9x zeCHR6u#1{P=evy$?`zF>vq$Lb0F6}?8yyyeLoE+%SOVgvp9)r#3LtxwxDh)qc9`x}Tl$S890zC`hTEh71A)C+qxb)h<*7j=$h+?^IAldQ9&iO@H zkI?eata>pKRrqp{O4q^!!cbw8G-=zh%fcxX z9NfpgD7QPXe>D(uL_PpR*oFgt^(l@t*%K*3JYrUVv4qHL+5&AT@^F;i9yl@&DWrsG zvaP*$kKJ*jEd?S_YR+W-k^2|AYU1>%1fWy92mo@b(__lw&H#Aac+h}CwW+rkE83ZL z-EOtwMP+r36hC)M>z3-X%2%RPMm)nuf-4l&ZDv}dr8}Ke1e4AX&=(T9$IZ9CE1Y4E zN7R{FFlp`n-rH0%r2@6URTPEiuGS4dJLcoi(;&aib+{D#QSOs%Dbj%74t*_oDJ6-u zs9y{I8rMoF?^tS|A#UU`IYbyv8P6;iAWX3<(pUJd2}&EhejDmhlT+UvK&V$5DBU>P zGLj!aXdVl5@U=%cVz>%130^s0(3*AX7mVrB?jYn6U}RUc!_5iMO$i$#>egF-Gu;CI zFp_vKlAI#0aU*JPrT2d=BxE6c$P^3OtUuS&PXlO8GPB56tP_tOYIjO)Aorw0O0LR=r#~$6r31Mh%ELSv0P{RCP zZx)DdUaz@rNQVH9kMh`B+-~vp4acZ%K82c(j?)!9S+D%h1L?x-iSH!Hg*xmi@?s}A z$+8dcU(qg%FPIub9c`V`8Rpsn25w(thPD1{`9sG0!j-p$yN{g&xxbrYu3nqG>9Vq5 z1x@@=KVH}lG+6|e>ncbKeD0Qdpz$i)YY2E-rq;49D|VV1?Tm&hK$T!W^4oEQr<16D zkuNjpb#(uk&+dR<)l_=_1M!j+2Z=?LJ#z1t*?oTLw}a)a^Z+Xl)tJYBRf~v3jN#}b zm8q-5u~Tm5M(W1t;V|3NmZbwvx#e^iLwbx2l~C|RN-U78c(}wign|PtbAb30Iu>-j zM!@GpZ*9aU%30M)0ut;4FBL5L2?|%2mTlVt z=uO0m9}?Gb6ab|-UcLys=e*7uOfTeq15kX2=MCXQhT>q?=199_98#GydzRi0S$P+~ z?z<(gi%hS&)yd@C3ooJ+bq{IDD}c$?{#F0VR#!cLyK(g)=j`*jeAeObogn^|l!|AOHXT zw<7g)MYcN!Z?ZPLFn5u*OK#Wg;M*wmbq&5^ZaE%+MJIN}&I^%1wNa0V4kKAeJpOa$ zuWL_T@teW?Y3(W$9`xlYyAmOozW)1L|9e-0X9m`VA*D&AA->-UL@%jz2n6^C@V`zq z=?Lh6EJQUA>NoY+?&4t(XVI;zb+k8W#q&UEQ;+=***~0qYLj z);Tzjc>XJj9oGRRZGUX_`F6v~Rl6@ej!Vi9DwyoC%>uVzhxgXoca?dQ?S5a)*S%?* zKYCmB=LnF(O4ul6_@F>G5!Q$=o0=V}=)_ZHd0Qhi2m)j~;$RtYnIB+SPH{KHVSMRr zd+|gy8FJ3;ofqdvg;VjRZ|qp3YC3|%CN*;>k)$NY;PVq5+<*=gQCkofNC;)mGG{Yv zU@o9X;$Q>wexBXmZ`S#ZQZzA3h`8^k7a;4^>5nxKUvA>k6~*R&S|K_q8aQswASj|; z%iIQ^p33RTpalN#@{#}`DoLoDU8+e7n0Jj?W~t*i&58>JLF${i3F2Uctz+Gg%cQ@V z%c!RlFBW*L-Ih=4idAjApUHCO$F#P6ezBGX!9cYlY@3=7E}(7p_;U9TFHRz~bIl-M z0<(>%CVc!5RQMAE66wm!JKq@la(==z zpi>>oOvnPz9!B-Fk;GoDd?YxTRvD|KKi6@^haHJUW&0*-@kV~$Rl;qmvB&V~ixBu_ zFE3I$cRDJ59`lxxOJ6)$d>F)S)G(yE9GzpJEOjk?mMA1@ci$zwt!Q~uv3*0o|phWtc>ey7BV*ombEHy*7q*q!ALIh9j zQE|dF(6OlKkceX+k_=yM($KtHtrE|hbb}JP z9e^{E(#Dui^z44*e^u29C%Xga(dhYE9Qw8CttiK-x|nm>wZBL#6`VxUlLmlfO{aSE z0HSCYZ8~K@l-paNcoGXY0xK!u1tc$VBXLTc@s5=!ScD7(i)xVf`9<-1NNyX?e|Y!6 zBwWeauElMn*~?u86sM4h>-~8Or2`cz;Uwk2ETVSDfZQHrC73yufbxyvkMLbhL3+|! zhOKa&v^dfjK?3J9;Cuby?Ypmt1|k?7_uaRt=*Y#T{*YZ2QSZcbYYXmHTx+ec_XD2v zPyOLF@e(JKuSw(_dnDTtulEM!7}sN z8L}-&gZX%y+3HT{KNTfvv{O&R$@&!YY=L!jhFgMNk3&GJO?=*PLy24;Y)AG;`y^k= zOeP{F2f|sf?lcd7k}jC&r;2mVWfkOz0g%7fmH}9==MYg=*+(oD$QyAIC1q0g~;16*s4AEBuMv1R)R)I3!6%qOzJ}RDjTI4-sY!2L)Lc3TMaIP8$?) zq}V{z-w3(F@bws~J<{)q3eS4}(;T=%Cd>mZ3xJaC8cs&9=9svTw!a1MTq3uO<*9qW zd<9k_X15I@CYuu=Z7e+d0+(^j;EjFR@K`;PEt^DXwVFfFZI_8H-nVP>U9cfiRZW}} zaJGI8SdLLypIPuMeo3Ci?eZH)O-I$m;m(-9`S4XY7(aA+aW&jQsbdHSv-G5*s z?2i-pEjz}b^s4ocqH$-kqhJ_j0&*;=$@7V=AdE;rlyqh)BI)elL0(<7@i>*Fw9Ix- zcZozHS9^zA{PpWsB&G;E_kcd)pyc!C0bKAdgi@)dwGTYgoobsWH~u_2UglvvwLwjr z)Y*0!k0O#1fDu*)y4ptIt=Sx81ONB09$7$h7Ail0%}UbHL0kZp^Ob!*&~c5B)zp^i zO=D@vGliSh8lYw0MB|(e@QJGAqQQ-ILRc+=8s$f+n_L))paR|kC`GO66gCfatSti6 zkUjd>uj)wGq*2rPR>G#QCszs_a|!n>X0i%Xz7{;)r#Kect~ev!JqrBv(MOU+zlnui z$CEGzU@j#W6v^4-goDuD4-de4vd54$fcV6p=13dx6z6#D*wl3T{zR9iM=@R~Lb9N48vp>S_5AMCTS|*1L^N91`Tiny5wV^-B9gVj{iP0d{IXDE zFINeO3tK=nTXYuv(kUwz~eTKMD|FJ1$ zh`EyK3PAr4h8q#u060QnnTv@Ea&=#FD*1b^8E{>Rk}%HAPNo8{`GEvRkWpeB68d%x z7?E1D4~UZMl+-XU?)%@rFzl8nasi z6cf8>j!fnC(~srAvd9Y(~MB<#%H4_Z@}+4 zee2Ke1HrA5cKV1VdDl<{t>b9ptdQ@~S4^`Um8Y5;)S(=5Kp+NB$pVcQ@{S170qIjk z7F&Rz?vidux$Ui>E&{;+nOq6(Nn{gJ6$13Rrzc9C|~giTU!TQHe|65}}Z{a8eU zO;9&P;Zk&l#m^3qQ%cS$I@GB+^lVEY>4anqLJ0N6Pb$PBcTufLMFZuVevrD+(B}Q# ziL+SADIY1N8|UYJyZ$>|h{U$u)&dW38GSlO1oTew@l;qq=) zZvx}^W%2GoRP=M79Mg4Nr1RrPbF?`O%`7c*CKO$Z!e6#O5HXF1QyK{K-l(>cFvVFq3u{3IILFMJ$$X;{Ow98{ud6 zUtt+)-BBQrv+gbIp(jy2LE#G}M=V)Wu59U!0WG{O5E)tZkf1_}$+y%+aeZ@v@`vvp ziQ20ZMk?&vbLrcwIEZb?n`Nz2!_Uq>Rb@?4!uo^WP{u7vn&*_LmH5)EN1^LJE+4|U$F9UC^CK=U3nD#jnFp+-C3zpO*9Alp7f>k5q9QEn5!s5pCZRLDqj&=!*G_oi zO(N1eGZByVpB13ABmxdJ1p|u)PwI*R+v)=L>0OUH)3Xn~^Ah~wE z$5VmIn}jlwXtA#Mp3`Q)a)Oc-{#M@LhsHgvOy$S2lE(W`c8qPtO;M%&+c8QSyRJY# zg)RZtfR@VyU_2Nz^@FGQ^uB3_Vo*iaAN7{x}x_)q=K`;``jp%w!$n_AQfu zVASMqBThyQ<00yum}E*}*bm9ic^M(!KwfWc05>6%CCvI7Yy*|R0Z>=%0m754xsE3v(4dyn zc;bvQ@YY+fio-C^BVQ*f8gk^!$vK|^=Iz* zw3QZM3lKJq%}dMi?C4YAe=x^u;>kNp@9N7|0OVQ;6U5qY>`FI0irnQbTo2{WmraO+ z*8D9s0pv|7fpjYY!SLUu0L=HOK)c$WTGus(9OExFap6z$ryFT>>hLu@l* zc)Q6snZzJ@`Lf9jKyYUExCa@WNhqNZ#j%4Jq^r8Q8W!QSQ}uRUor0~a7q*x<-J>N^ znUd)j7c9@$Ii@^0Sf%Y6myluDn;s5irJbV|APuHSO*9qpBA@~I8i!QC{l%O7veVBD zvaEDEV8JB(!Pxp_!h8I_q#Hux+VsxWKXtC+Fs#L;524nWAza<$U)OxVjrAGv0ZDIGW%{m+ytF9p|`!5@L{q)wo zDOeJpReI-e11Z8FT^#}$jOi0 zDKhR7G1hUJNDZHqgraKL>qs_o?Qy70kAWi&9^oS!D3ev9>pc!(=HFrowCA5wfTh@h zuBV#tNJ6k+wm&MPf-IvT_UF&D!~%1yUSmI)ww*_L50tZZQqo4D^7~m!uSsP*yE*v5 zLS_6QIZ8{zwz>Z3j>a95&0!o#uI!^e7mEEQ6Gk6bv8vxz|HxJHa?|b^h4v>4T8|jG zdgRJf)Sc8q;*1x_qAFx_V72UrJ0||!HU{;9_g#flb@sM;AeT%cR0J$XG^Pgw_Ukdq z*#V$gbHwPVGgzrIRJsSU^}iM*jE;r#byLV{i4yJnM+HCz7fdG%xKH?XCY<(do)OG+#?B;jIu+yu)=AoYn^vVU z3Va_<_IX6-#OfizYIbXGPGi!ZSjg!e8k=AI(jImT9|N#mB6piw0O0=>zMCZIf-6*# z=VdeN04ZYW-`cI{yZ_xc_`kDVk~#t<%TXuyLtUnZxiZu#OwAmoq{ykn|4Fl~5vK|Q z=?@Ze{c3D<-pk(wA&ysIDKwv8Jn)tN)QAUPfo$Ek?~nvdbOklDovcf2R^vgCtM=^Rp$C0?23p@tTA##Gp<@%*Qvr|GBcj zgHIV%ZWz$M95q}x*^9d1DYybe1^}u%CHnE+g2lhMNJ!mu;-v`rVgxaCnL|Zd{MTkQIZNu;^K_p6)IUeSE~5J5}WH-h0PH4%7~LeOV9bhz9RyF zki3S+J^dPmIQ8JzS8~MAnd6t~97~52juhF`&kM<^B4NFh`5e!hU4EhxXTiKBR1V<* z1rjQVa}WqRp4~CUpVa8XHB5cd|plta#nDiyRJC$EV~ zIUPfh=bPEO=}OEyY(L0LIlY#HXBbtn?zBJ+GsqRs>%_y38C-8+{tpeoCt3Y2-l#~O zDb(BpmA67^Kz&o|`2nG@R6J}ld1HTxN90q*VyE)j;e6R zv(p_55xf&_ZL8jV=FgtGnje?*U%D{IyGz~2^%-G)h!EjX~-%c{#>AU#7l7FaRHXD z@lC|J!QiAs)gD68%L-~zq4Y;ZUtG3>U|}rd40}bhfd=|f0j|OZKtiSx1i(0)m_Y5EHPjgkYoUXzBonnCw`m7f+boN#HsWbeH*J*E0D!YE0&E$rf7q5rG>t z>*kz(jc~?6`~#V;`h7(u_{06^R1AWz6ZB_d*V1Y&&V~!AWR<-8;->5%E_D(R;dF?pzo666#7MQ#hB}#!zh}8E%O-96p;1kS< z&VrZsZd0(#+U4oIY){)pAfo}?iv=)_A_!(X+=k51GfGUP6r*99VCvU#%<($BZ_|I! ze3ac>{;f$A)G2u+Do+}e@JLsxjjunNRL4tyoz#coAgm8flcO*|s1;b@)99X1@7}g; z&1bxls6V67TUMgG#46u1)BZJ4EfDJi*o+ATMUNRChtL%iU+R>{$O_5flC!e0qEZYzwkVEc)xWg;po7TLzz6Mf_lr8g?3q15gKiBU?9 zY9u=)5Jn&d_mFkvHlD>vRAp$&xK8vhb|YMSZv9B>M8N_dB2bSu_qhka4Hv@;7wp`) z2pS8}SHO4Ywo!40jO@#nx2%3ljpjs+BPpIDHhu7I{YIEzj8{9(gYg8cg$8$GIJ2M( z;>Z4MJ-7wV<9uq87Koz54BHUakIVxZ29el)tBbG~uETFD4s>mdTudbt%q?t^CsvN5 z%}{%s6dL|S1wa0#Dzp0ys)#(`aLNAuQ^e>}6ckyAUIRs9;cZ~H4a{FWfn1?$iE6vt z`)w+s(X|FBXog+#cU*W*kBZAs8`rggBa^TWl@Rv!p5$=JPEM6KC^us-U4A&8a2tYK z>5sFPT?cy=4MK%fnMz1@Mo_@;*QKgS5wvZf-Y`8KC@Iyj#N-&TAb;Pw=nyWVW7Xv% z=CE}zEu#OKs8wv)Xy&_5wTHmORX6HaV%wip>`A^qKQ2;QPo>7L^NA8+G-@6uhpIDT z8I;97Gt@?bawoN^jzl-67>cZ&)H6aIu#|g^?YM1J_HB41>WgAj>fN}r#V)uf<3<}> zr~JWWOQ8kka8!~C!VKC}nD!)*>|v}zJA~L`>Mf=hFwnl$4yqdM08S;U z*9XJFXRQ%p7Ae>mQ4^i|8?;9f<6Ht0&jH9&1iHu=g@cmuOe@;F#GeWg#v4jqHt1Up z4WTwQoGl^*NxJC5qI26oml}Vk{?n-Lo6~CpgA0418ON1n{Yv^N#YE^rvPDf}ThI1} zpj$u=b2Z9QGm?Rv{G;Tc8Nj(TN9%QB+K>u30aA(q)ihNL8gX)oz5^GbM8^_c)s^5G zp*)Q3p@UBnR)U?lsq7|le$ zA%@esoZK;}M?2B;TT1;06#UTp4y{U~A=5S-N_r=SjoU73Wonc~4V$SgHEMy+f0-;j z*8_Dg8q5axQv92Is;TgbNLQal4yPTm@yyG=giIJi$Kib&_-Aj=TP$aAs$eups$?Xd zC~30HGfm(qUJsxxnH=cUwMK`njet9uvEz}k$R;kbxBx_CCs=@D@C(-@67Q}=-T);c zV;qhxNVoxvZZuR(HDi(Xs0JWIYHuZKv<;LpaugZjPF~UN!8t7@frX7HrNNA3_$h+@ z$%|I$ln>}Z^CaS~4N(J(1w%8_WCZG{7Fjg6EY21^tQBCG4z3eq&SW#3I3v<-s3nI~ z7vIv!d}=!=1K}M4PL$2bKHHbS{q@EQ+S5cDGC%=TJ@LDN@Z^x0jw%Q;yxXEJMAYu_ zil|rbpEvT1H&-L3!RNi&8J3^`s|fpxEOd2S&<{2zyDeBdPF1b9Mq){&}zSCDmib_g7~qKzVI&=u3>Y? zs6qjP{2C;3B4!bVOyn#bZ;~#l`AqG5Vdk3n(a;0yk*Iyy@XlAS97o-#gy_NH;(M9e zbsl7N>p{klK5cDsLLPF)Y@giV%$&zx9FQzuEHt0`!-zrRQrwGk=mAbluEw~PA??fL zeOK#&+H*Lo&K=jU)F5 z!n3_XaCZ`hEwYrFV~x>O0t>1~!RRBIIOjcGtNCZk&2f$^k*`R)oipf$K{<99VB?pu zbn5jUm10l>2#lfaCLUo}_Qb&m#`Bn*&BLZ8UeKYday8Ht!;C{!qFEV%@UwHx;9Gc% z5-Cr8z;{y-`Gr$+!|rrwrdI|%7ul9)ExhXTvDp|Z)QLeA);b=2ZB?1}sT?KTXxC;jZ z#%)BwQcIUd;h5T*ByKg$hJ81wIt-2hXhoLyhHC^HYiKmUxJtNbz_^0;$Q7@h# zU}Ze%V+>YBB!vK%x#$L@H6g6)SjzNB-yr+3`>t9I?IY{uJi?t{XaU*#yY`HN?Y6#} z<0nqjT|`}8NBDhWrOk8~&zCBuE*IQkM_I7GFDVIH0M6+~`?Y{KweoF<&KH9Y! zq{1El?N|nCD{WUoF33tGq&3O2Ij|mAq0ANv5)g(4SxA#|VD5R+N3#B<#~Aw}^O1C! zbfoR9@8O^ql(99Y7yw4tL?dLR)k6gV+VCY6WYTsMDcB6n(|yEHlm0~_x6$)6!0`UA zA1%>CE1)z&=vM{i_GObhgDB1;d?;d_KzS0|Xln(yQkcGtGB)ZFF{UrCBeMH7AS$Wh z`~>{%z5b(3IVR0D8iyXpbpYxm(w<)f6(wJfSF~E}`Y34WEr-fo4H)r1aV@7G&GguU z{6Pqd&7r!B{QVhy)Ds0uRm|WAn5_&WKIuvqPse=yUw1Rh*ik25l-Am7?&1xX(?Ib_ zf2pw)L1NB7whz!K85$r$Q#;s2u#Wgsaab~_#H_w&P2KNkM_%#2hSaCKm*_Tpdizd; zEWg800Es1=*0D?oL*XIJqQ6bleT`{G6znc%Z<&ic4l zYSlk>4C!S-kN-FrFlHq#&MgskN1pVpQy+;{gLZ_t0GJZ*U_xIC&9=hVjbh0167a>v z!rQWort@HP!aZZ2)No-Z8AJj0u2X9-c#S|Xl%O%g>FV(+ifC0xwLEFYlI|(=1Q8y* zLo5aZ3Xyw2&^UK3&vM~>91aQ6;+LUUgP8Wwm)rn zC3Q?NI(lKja+BF^9d!`+q4UZvu}<4>m~ko)F;nKWTg!E9_jck)$o@eRqpq?*$n~t* z?H6{RY^HD=+Yp6z$Dout?rvS#B2GyN)WCJLCU3^{MAH*ZaEA<_Uz?~OXLR{8N(m(u z{1CDB6rq2YNR0W(LO|+j<>}U!N1#5JK#z_tauxm|Qvi{NSvYm1h?_xzHllej_=F1D zJY?x7z@3k2G)AZYoJo!?>~ZQ}B>!35hed)gsN(@58kx*GYE@g}-Iu?UpY!0UZ`0T` z$`4)^!v-lGDEN4L9m51>$Ukp-{{60~vX5l_ZIJZ>&zGle5?)M#C@Q<8ykSG;#pGT5+lx=& zuT;4-=!_;qNtl;YbwZ>rwBWLjlsi8*<@9Tm+8THcT%s13G6u&;s?La7A*qDKcg2dD zTo;4__xkBcu&&)A;@Nei_ar*Zscb>T90?hP8N)+{hq@4&V;G@j@UjAyCy9_~JUnt& zlVkZd5H?XXjAIlU&V(J!;AClOSnAf?1LUzU2XR83_+*$STk~onoT!1*9Jf>aX>%ZJ z8lQN3Y0*Ymu#Dv1rY7g!0{#?ns{*1-5l!Vp`RMM zwy12{;p5bRTasMvGIbN6u{Dt6gkAIW5+3lyib3mzpqSi-@2PU-5PoO^A9cB?WNHCVy@rEwD7XnNDAZ+~TBCP}e2fPHn}~xVBOn!ajK3ZM ztlly%f%a${@_s1{8^%Ge5Kk*w5jlD14Oz*C)Ia;bPFj*xjdTziuSPxWKq{%UV(7JU zpnh96L_{Mlkxf}l$!<`kdvdc55+X5qG>eNK;TotzFQ==Jt&fyaoX+D!D`6hu4mYkD z^~qqO6_+6Cy@E}jvBaIg3KNpkqvF)n(BhZWNUczSWs}1BiLS zW!xBST>z9-fxZ3*PNEFQ4#CO-lLX=zGa2b_7j_O%K}KXT6C9AV%V<`I&MQ3FJZ8Eh zs8vE&A>cqIvb0$S1B4(MUXrog8`Ch5Dj*Pc5l!JJ?p}-@J2KF-i;z3tR3{p{MkrH@ zp)VV7L=AoG`UkD))W-Q*rapk8x<&EjudD3-`f3lGL#`bZlIzKcL7f0lL1?}sp%&mq zYO*+v8kzT!H|=m4IVJvuf1ZRznw6<{(3;F2v3Y={GAR`#j>_3IZ6+fj4EDZYRpWX6 zbQyX}YSxI3Wf15^PLF{P^F}<4`-AhB!$k*?4fujjmihqjK2lX%FjQkN&9{T|7!=$p z3LP-Agy3wNQ(9=dJ~cUxId2H(@Lfx%Xd>It&TYp@D1|r8KXwg1*VAsl6bYNWi(vRt zs#^~KJWsgMcFJnN=b#~ari!5dX#?-0b#5Cqc9SZ@SRz1R>mRWes*&dpwZ~F$yCDi& zt0xyx#lPVb_f@8HoI`RVS>H#IkjS=6!!`-QkV-@K2cjSLa`*t46}LO-x;k|ebpcV# zpjtZ;Nc93SMMWCpH*HgBUItVK8pw3|Bi&@QMLPpAjLI%ue`3;;ACl%vDm?0H{dyNn z=PRWNu*J1AJZp5pH>vfYJdw<3ov7lRzf6#B+FT zQSonPQxgpxR!j=qLeYm3@yUS&;PlDo4FnV%g02aAm1XtmpO&#Ci*OQ*$Ic!Hp&-&| z+ma4_l;B2OWCqq`H3#U!51s(*F|r~+LSS>qejLwbY$tjOy4>rqH!Xn^)0pP_K$>O@ z{bY(k*JlUeOX{A-s#(N#27@-G|2F}VaUY}zvIcd)r(K3nfuE-7VeM!;GX1UMb5`wK zJnhGbyPTq4H(vZo-6=~-r4H#+@j)jHlXqT2JIW*85M=vj#pQZz)Mx2sXX2&;Xe;5*3h-4Fey4L4Cits;d(Bx^>kY9%92R9L2g zBnIk94kNo%*@G}x!yz>Xq=@usH|qJLV@x9`nPf@Ry^w57ZdCBcJ@_)5N>J|vMPr%Z zyH-s1>-lB&U>w>p2ktS=oQO-z7N<6gt_B+70*_-Skh{rCrGa}loM~`j2L{O$gU-D_ zyWp4UEMX_~|8`)mWie27I7zx-=3R-BSG4aSt*Dg18Jn3krR7=mkv# zY(WVv7P|;r$sZbo-mn_grOCTvD&aYRxN(&(lGsr4Nj8DUbH1aK;Gwb<3970*C)3bX@a2FY1@;v0Mf^I)cSx9PcEDE7Mk5?9z>4^3@6b* z+^n2mmf+b??+s1u>zF$G=S&f!KL8nQuiBH`N0?9pf&{gkIKy&H?V@ls=MT2dqjN`2 z@pd<+lCjDaUvRM_6A3aJF@@x78l6V_52}U<`A*16hS;ScI}#H}=MTj$u^d%Ui^*vD zd)zEf`8`k;`+?q>J_I26YxE#UQ)eb#qxc#n;0VD{vMK~zb_M)qVNlTIN&6#SAIgsu9Wc$^T+Ums;5NtX0xku=0`tJ5iJvA`W= zk!kiOO+}?4FjM2Ld`v9zcNf=2_MQN$*F+rz^uz_SwHDI-SSgIf_`i4p+UzP(xk!@a zKcmS~v6(}gnURQNl643Q9lIxc+3nS31z8LNf?h)oQHj_cf>+n*|uPi0~`Pr2=^Q=$_73LHIjG z?c*x;RP(y0=O-XKFi4x`MZ(KQ<87F#98T79>;bh&;+CnN#Vja&QfJMPvzU^G#&pN> z`UTg)OJ`(z@Uwo%QWDgUMdrVcHVO_GkC;7*nGgourjo+GY#`KPVrKYSbLPu%3e!~3-XF6ll^QLPoFryb*CN(UTr~~yt<9vco zrBDJoSkun@3@d&=F`DS(pmr1DXuwAr|LrdY`v6$>-~+#pKjTc>19v3GLD)R-w{H*r zynNz2oEcIf0Ego6+Z^c2Q(hH2Ktr+p8cE88?vcXu{`NVbed4Gqg4)AqTxTZ+qVWT; zLv@U>KBBm!ptEI+9tyfQOe{%q)}+(H)4i~p?{9%hDV|nRFl{5EDzPUOBeh! z%vmDy!RDJF4@N!0B@#P89EEuhZ6~rl8l&G1Uxk(lI6_5D+fSTy7jQp?DNr1>@NOcX z1_fyjY^@GSMiu{m@7X#q-E{n zw*>trSjbG$muOstEO>4lQ!jTEFMgEp$Tto9=yfzT={R;VKgp`mOK(yfgk~KHXAq-> zMpE%j=!?WVn&VDNKcIm*I&B3M0oDKH8Kt00)S-~FU?Y2W$HLr(^nQymV%qy$-ilZ` z0K8>!@0TxVJ{jSjxtQ7obHQG)Ak<6-FXTp$H#C!G|DL6~4=M!Lbpu|O7f&F>UI+N1 zUp)Z7`(7jzJKWfD6kB`2liaX{c?$0yP7DncE=y{oWjmv89>Dq5-8K6077einI~F(+ zlz9u%Lrva>kT0YpW0whd0HXiyPy<2*39}cmj|@GMRnOpW5~Bx&j6_a%Kb;lK`xbV` zVY`6S!0=yKSztlK2WxVv^(aM^;WSwW$8>T|p9uRWqiOsZIzu!S2Z#!N?)a&eyC@d* zO!f;d*6LT)Y!5BIUKpKn!fN2_}-&#RcBs9e0U%ptVTE{g*R8zx|OTmCP zUGq;5RWE(+MJ{!=lmlu6(g-EQM=Np>xAA|v>HMP>72gmTJ5a@ykr)BhmM2~!O_LFb zzZE_A{6}()Q9+8HXBnt1#e@uTWAT+Pv^ccdw?I1vjL5k*AM- z`2nfCFodt8bU?j?VD>lR&%ADJ5k%|dZRnHZpr8`|F!`~WJoo?NQhKhM)JcOSb@h$Y zE9*X{q5{jM^UIR3vLAoD%n$!;4dT#mWB~_HW6X(%qOk|+=xC%Nc+|^6Lmdd7s>Tsa zCnQUA^Zhae*&%r>CIhp260hJlooKJGMgjGS4J2c@5-2wsi%Nsx*!DjMqn48esZgdG z*U6Nv*aX@PmBZTZj3$$tL`2avd2S?F9jZM@xRiPl4Ff%6>Vv0)%>n03Cb=%8Z-dqsizF`_(4a#TcPqQ17H(7$k}bdVpX z>pe~KN2t`bltQe>B3P2;4&i?*{mz4^ash!GABH2r^ipD9me3d~nB76b+dvXzY=vM? z+fh=RaD=2AfLAFmSHAj}t`mLBZPXz~b6MF%)Xxf5KSjG!kU;b_0o+_rPI8%o1OYBx z{g@9TB-pX792!-n?_$kp6#sdLqv^?XGRlw@kIM#z*+c16HTYxCTppQf!7r(E$*&Dg zrAYR&HFhNp@ga4@}rq7;!L{Wd8ufhgG}4iDm^Jp%lj zblnH2A2;PHc!P*7{tbxSCcF(eS7%};@i=uAjw2t^3K!2;S(yx_OzgOHRvTA0nJUTwei1J zM#kLmp585w+-o-8@Y1V?BMq(?A$3NB0}RiDI1I-1;l4ZL`0x2YOwanf(BLF52~Y?XaDlAFRZda z`ej!4L6^H06l9rx7TZWsY8-xMgAxQ+j{Y!Cq7Lw)?K(l2#Wnol(N@%^1$WyJ07n-p z$ePiCPV-Ao!AEhWzc8!#c2(EIW(9yRU3|Q)#a(VB7#_nC@FBW(b}17RMz$@u+s*MF zaqg2t`je=FZ2b1As#d~Cnb6v^aL=bDeqP@0+c<4A2>C-j&+brnwSRbc_@xaR9upH2 z)zO;E$zVq?A@Q%qo4e-B{OmJT?#oxN(%r_L0@@cYT$pZNBK&5g>32a~!MmOdP;&0n z3W5#75^#U7 zS>1e2E-Y?Ou9QLYcKt-7s}8TOMB}PkwF|(bKLH=!B)%1oW}fI~Ev`<)j+qbs`m0Gh z{L}q>$1kobijV_2rH9N(ZSqa?A4JjUO11g(&<0KfkCya`72iV@^B)?AVz$xkAA-Ij zyoIM&Tm7Tr_C1)vrib|@?B;W)jKIXDVGlW&OB>z20nWY)V@J3)P4a`_MmL|5S+i#M zrr@%%K#0p%9{$@F>OJ3-`0@y_HgzgHbd)Z_RmMPxvaGs&SzJFc9zOY<@XP4!*zps) zsG>rX9%RH0FFS=3pKj#2e7~dJi9dguN$8Y@CBJ;&9fye94Kgk9p$-bH1mhe%Sn9}a zY<6kCj?T54H*Z$@N$AC2N8{&Bz>Ri|8=u~-P@+An?FsUJ^{qV%IF;B_33#Y7I|Hk$ zb+)0SatxGA<>WXdzCmQpH()UJf1EzkMlJUW6pNkk1lAzA4gup@fz~GRVz!WYq^h5I z7ovJCtXaA!ON_h){h0Km69`f!yIj7TWs?kM#(`HlEJJBeBdGck972u$%WL z5@~pUEHaY)KiGTksH(5ETX-AOEWy}8DV8m$hzN*)Aeb1H4Ty?LFHxHG-b6}r5{U)@ zHv%Fc7Nmppj+huhq$x-T5oyAvC9#~t^t?;H0za?VIp_WqT%o-*fr z=DNtgM|)S;A^d}65()foc_A?0g$oz5#SVZ6szH}+Mc|oL)>}9F^2t7|zhI_SW7?*kpWiop`1Xe}!@Bk_9|Z}sqh7tT_2Ja; z%W7b}6)|Q*>uv!70ZHo?&0=oasxoUgf4thg@|O{eaBJ+#;o;#YTk*jeUbCKfrmd*# zxi}%4kABZ~oEYv3U-W*&1wXhh@{vO+R?|XUx@&IOGzcA~Lx+NxnU;;!C#N%_*mv&O zRDS-R-Ns)J@W7egcXjshD8)N%!{?W6dK$%A?lbBF$^-cYH{BNO`uF1t#PXo}W(6pb zH`eCZ8MZ>IuB4>IW)iS^M(R=_re?HVxFjtbUX6Y6&BtdFaJUl}3@B?9GQ=`&Boe$Y zI!REk$WxDf95$8o{<%2VHTBx4)bbtVH9!8W_gTTJO+HLBxDIKj{P+{PXi-C7Uok8e zHBi!-p3`T3J$KF|)?s?mi4G%TGZhpRGHu#soPGZO`ksj{XGSFisD}`HkDWbRu-)Kp zB9h*twzf92iNF5(0X>8=8~2v@>eZ{}m0Lf4_ujjgOau3sEnKnr`e$D(G}7LVpa6tLWz1?+3O((y*0l*2E*uNLn==2O)fk&1PH2RUm=%g3u+`?BlnUR(|o-SHjZL znk$9XkE2u0a30VO83nF%hqMqVACAM0<3x__$%(0{1e8g6aGW5_=Ovoo>=kRhWy1`t z-#09H;BJkmcWO~aAk!(bzyJRG%U7<%qDQv?A_4|18i?7gKYv}6-`Wjxr)Z>)huntK zI*N+F`HG>uI;x`Li*xLfAa(klipjGc9ugS@N0^tgH`NV&v2ewaAAh_Knrg}Z{rk6U z-KvCwzh=0r>;#wroi+wzP`c%VCpUwk$`lgfP;-ILM2fnNAhtzbUVp)Y1y}dDXQBMa zaOhDT9cYHoJUS}sA@?nWG3cJnkPI%Pw4t&NwP6CnAUg^n-U72(T+OH3?iNyJp&}_n z)IeQVH~htm7mhRU+&s|GFXZB?gSzz%&PY8G7Z=yqx`Hvd?jKi#`N%^k&4QSu&o_To zkjpGBDLLrBXY7jFv17IQE;b^1dB=gf9VPLHdA4o)pBMc6)X|eyPv`sl`=eaHji;s# z(BC^))F2rUd@pjb#*rgO^3u@b7TnMJeZ`iWV7IpJ*l`3rLx9`-$YZWFnP>Qc-UHJX? z8rWN;FUw-~f`3g5K736sv}=+vsQW3(-23U5Up_||W6hZ?_!wL63WGGrlU0qNzo)5a zI^z2+TefH=gOB;rBGs23}!B(r%db)Z&FmVmzr3ydgUZWnluWg5G~(=ld~#21D9*@{O5A zF2wzPcCj z>p!jxw{unqtE-^8tVJah6cBJ7 z?e`(f-{!3n)lWnd(F?>n=g^b+4@DI}h=Up4^G}{US!a(+sIed8^mB|(C@Eb4|9$J% zMXMj7lBAjwfLz+sbI8M#BT?Cz1Io$soX$fxR%A9l*qY6|+FN$Xr)*+-Z^kbI&tFEw zv}k*J{@HM}fJcF|O1cc{PE1Zx9e)Nxlh$!bJ85j=QHbarl#L#s$r{ZU;fZ~^OX(x& zCWj{8wn+b)2NP;~n?c*OhJ&9C1v-}yI+OI*V~F0JExv1u!rNg-AyT%N zWIx}FhyeCr3rqX%+1&f$#oS4nso%5H-hD0Id*DiMN;wv6_?Q6%o&Ii zM_66UCu1Ll4S;`6ojO&Wr0ibu7|OcNo*oiS8}}ZyUn^yO9NsjN9+Qp{a&909G0(D} z#;)D~I8$a*Z$K_$uLI}j1X7vt;a7mh#KZ(mgHa4#v3&V0)U?7(<_`!qq%dH{f$c<; zH3@EN#R1ufq#`WTvXEBAp5x-{%Xml1oyDcMfXknTs)`+T{rWo8Ia?7Hv8gg2p-(bE zWq#b&);9RXd|T6vpMvT39}XNa$4*%zVdA@Lo91;iyrpez256(u))nq_e0k)H&p$Vc ztH6S=n1Dc+dV{jzu>}Gw>e&HLCCA2YwrabpM~fUwbOkqeECNFU;K{ych57t$olCLA zOt$P|xX6E61p((%Qg#3cRYC-`&#y$og~PcOeK>HP6*g4KdePgQeFq|I_cANSEB%5G zc71qK|4kv}y)6fQC%mBJ09bBC(beAGZr+p;2gSt3O`A5gs)Yf_s)q4*Ja%s^QU=1g zx~**r;$kcs05deE)|n2_Sxiq4d1;_fUWwA~x8J^okB}0mr`fd8uO%4R2QDA?sP%5o zcR9G&gezCLS!!6(%g`vJW2WZ-IQ1!7_2WpZ+0E}zxT1&s)v+`zAmCdbLwd9LnTl(B zbRakW1qZh*!qeGpDYSdHHuCXPJU}K3Fa!0`yR2Jcp%O5kc{f2V;%A?IX6*q&W$^1y zc_(MRjEok7)OD;ywAovbE0tcqeyyaaNSkrxs#VeGbK@Zw)iRgbv}u1aH{Pl$h?yF& z<0}LN?&Rc15405-V2U}2dFYy)7PhNnLoqs#r?-_?>*d&fhq>p+Q1Ur;JzDvoq=W-O z)SY+J)0w9pEZhT;bYsgxhPUjzk74HCH}ugEYiwcx6ut<}p(tzk_3I~y2+_!4y6g@u#uTDlt+x=;5iO)fX zKjHV+myCyAp8|cTwCk(uC=WKV@F8~E+pSs=omQ@0%SoNNb@L{m2^C!|!0IH}Qy83^ z(R<+h{N6nO2VXEAM*ia?ko+=rqg8lfy87!I>r4HxeS5)A1u_BP8*bH1`7!HM{@IB% zlrnP5)-u+4{e25BuuJRgv3B35nU;9K74a~k$LcwPI5bLNd}srMF|qpN3tqQGNmW%H zg3XnDd?H?6UKsg1hQ(4jkiuZNFo45+h-D8ssKFAuP&ItGs`$&7SB&=79K>tCOhLKg z>TOJ?=B#Y4@Q)aAw3KNFBmtf|-(Gfci%Gx3y75nuUi_q2NhMY*s@FX}U0@NqQdHIP zaAUEkugbb4ql}a^EM80qhjmH!Hkr$-bbzR!GCrf6+I1NV+YIX~m+wo4>o*^R&t5^0 zL0iy{-;RzpM-oI`5P07nMQk7w$xLnO(xqC1LIZv`Ks9TgzX95;+!zRNUlgYZ*S*~^ z0i{ZCN1oni^XH$q`NR1ZuKC%eDBT1NRjhsHxo-OVTTmer9&6Q*@?BYoXg$ecZup8F z0DKd`jPdt&Kw#PrG)~o}W!B=-und>Q6`>LbJv}{1pcd`V z&ToXwGm5=Qc`LRM!Ela_8B7$AEGTiGg0kDfS|jJ00wC5|SU`Yb`&IkLZrOV+081jr z2}7Wc08EPhoJ)Iv+`=VG z>Jee?Kx)|sSj)UR>O2-Je8D9NTd2NRQc6b=YhC!a?Vhs*kQLbXCzylJ`0Vd=Q1{a+fjlb_@gDi>TWABi9X%ds2qfk}E z?fvO99GK~cD&`I~MDp@-iZ}@SW9WLw{$FHA--D-wu@WGNOF%<%2G!NQi1W%{*ej~3 zu_+rvw7GZX*5l|L0;o5FgCBwVyS-)#N~U}PbAz>dihxp!f&j@TxTvUT!-fsCy$EI3 zF7hlanR+jv!J3-*%7v@T8=4Zb(Zm$AMxK{~`&eQn>T2peV0n3ikVw*rHct<<T1;Y=<|+C{X!8$sm4#F}qn&&IYn|76na-22ULj4qBZ_*K zl>UZ>JSU42$+}vw1NI?g8DXgpZ%hNDmKkR!>{~%zHujBR{(xuMJ_OZc*qq0KvP~m~ zG!?j|t6r1a!n$zbf*NW!Y@IJpATR1r-kkg8{qEP!03S$3nw1uxh&}QhIEDy7L^eqZ zctDtBjf3~&OH?^vD|`r7$CKRL?4c2-wjV>$m$Cg4ATy8B3AuVT9#M%_7A0G#scM0w z<2-DwU11{4|M<3_T)5mG1M{XUVX5ui>y5fUe7hKv1q!ql!VlBd)a7f}CV(3;O=v@} zVg`rs-HtBcAyod3 zPGNWG1>^PQg9+Kxn&bWwnC`NfJWB*N#*!Lb zOvP8(@OR&RN3HPa6qb&|PI>pSA#SA8|MGKO{Boy7>XURd5F|qD;D>SP{-lf*zPZjG z?WDEN-xoN3iFY{QzRwb!GQ1bt1XZA-NnKc;ZgPSLRl}t(4Q{`UvbinT^CR50_gTQi zfQW4wR(fLBvQ0`kCt{3B*>(1omWd^X_MMpa&fwpqardm-9*k1(TTBt9g_V8j_HN9D zzSf-m^k^KIK61?M+bgxXxVW4X{5)i)CS{D#W=}$glh_6eO%D(!XA-B;njc3^JFMyc z@kj5CC~~?0 zK0h`8HvJ7F8##E%f`tpyf&X2AnL2^F zuioq24`PQ?f2V3-2berVdUTH;AF*g`&G+svYpfrSLZM3Xi<-HfC}``6YJywPIQx%F z8a@0LAooUPCqG`9n`n#N;NMQa0%likKC%~Es&rt7?b`mTT zp$;enzJxDm0P-LHo$+dod@kR;h@1Zt+KvBW53akfA@vrbtSDO zg08zL0Jr}S0?j_!33Ow7il;NxweY(+GM9$rOKKqRByQ{{=P~Ctb_88xI^TUv(bX2ZyAb!ord|pRnks=j&dqa=2>HYV4DJUtQD@-^QZsi(-kUfRda)?hWee z1HQt6p<1|Iwqr-C9mWNpfWoW}%EdcFCl!r;Krzp*{xKXvUZfPFZ$Vn;lr**~7IW`` z(&egx*WEds3!`oI`@cev!b7QpGKA=*?7mqt`%wRTH_om@+5n?di#-AflqgY{ z#oX_K!>xL`ryg9OINJN{m}7z`Oj8_-W?}q56mCoM;g3m{!5I3^e)CuV`|MW>*UFD? zz?Fx+PlmZ}-Ck;5z-w9OUVQjak;hP6L<+UfMP8ZV;Hi&~nepuY-vt^v*nxe2U%`LV z&f;tSZv(dfAFc8KSe=i%>;FfKe{b;rl8;HN9YZJZ2GL6oVM+st6KZXmX9vtQ8z?4N zGl?^060XrPvWc2UQ7!?Hx&kF`ERCM%*!?uk#c=zk#CYhwJSC&&EYDm%aO52jew#OE zB?0tdvevI(ucV^#6lD+LPt=hU%h3Mkujwt$NNMz8=(@qz?AzxA6vwJ1=O_@$r|%|4 zh?WMlC!JUXScWKVio{Vrf3axmu3hTF8p%(A4@lZR-%BtspvLU6_CoF{KF07Wzp`gO z9@OjKtI2Wr+FOSm7&r=meJg|#x0Qmb(Yg_~gzleEE@0VBxehVZxOQcU>g7dY(DEf{ zA{;?=j$A76MVkRqV$#lwU~>>vFHy+5XwISvI0M-~Vdl`m_MzUVgR=0`QBZ*@vv(Rl zTH%3He;XS=*FR6sY0;eU{!aKb3$Sh=FYG8wvg15zCNJt;u zN1$WJqk2G{L^rdesItyOJtK1V%+LeRue=L>?MNFx8A;8@!vz4FbEsr|FHWK{+?2b( zOyf0EZ!&wycTTO@tZSe-MGBBBLAOZYsL)ihx68{vJ9BLsN-HVqM)pmU%O5EMj74;F zb||O1$&Se~Iz@Ox3YScgx_+lY+*~kB#&#}%d z1SYe1R4>y523D4}5Dypqmn9r<46o+lQ}jzoU1+4F!ADfy^%H8o`yQeae=4e*lk)Sw z{w0w#WUc#S(fqyo_w5>|QmQDKcCG-nB2J6gLWarw%S>jjq zAx1#Gt?rn7zXSd{L*{SVg5kY-Jw20uCssO*n$60R`~46`!QK&mYd;( zdhj?3VjU|82Zi=|Xjs)F4(N=6gd9-+pWiBh8(8{EY%qd@DIcLDIQRZBkOdSf8f9<3 zr7SiN#_ntz=q&Z5=g)r(8TA)XNXAPtk=bQa@*j(E3oXLiUb8doD5(0(P;v6qRpHQT zzu&SXBbpmX=c$9Xo za^CC2DYOZ>d~5&nCHw^LDErM&n*qKrNbCS6AnQ?vGg;^x2$rR3s5vJQRB|E7gCGJR zxG_p^$v|vt02>87>jZ3wr3O+(655w-*?j!`$+Y^xuH9qjAw%7_Hw3-QD8>Pc7synmm#XMmxsYIN75C`#L2#Ek3ryD%Q=NN;x+7?Z); z^A5jw0G9&%u{> z)96EMQIK~zOEaF2a{Cu9F|?bxG&Y-#L7X+v_FDV=tNA(P=oC(gZp0i_x3hH4p*hAz zP*8l8J5O;(!B4V2v>WGs{bxnI*$1n^46E`C{0K-Uh+lEXH=pD6fFX$J7f9>bA7B$hO&^BV zoy~kuTx)-!=04O1% zXeKF$Z!ZHKs|WG|YRErEFiYH7_K&;z{@);p=tx>jB51nzNrneV*D|5q;qYu-c;6TiDB1&0!dN8gn2}LUTwMF3|nK@2fRlzj%oxWU2rBUfdWNXloS_v|>Dg2Q(+> zv@RHlK?a*h`}bFI9ZyuV49bdH5HEbe%D{r*l43k9@A~RT@cCWM|JX0z0ki>mo``V< zXewW3p_Dxh;-~kKE{IkN9hWv9WVYrw^uQ&QzG-V-2Q$wN&W?8t6IU!;wrguVPi zq0k~p3X?_h8Hn@krPhgxVgwH({au@Z*HkP@5q1siLac)9>6$dh8GXP9{UA)@uIe8h{nS9HQ`B2ep0V`Z+qsYD= zq6YjQigL&+v_QLPpblMu@)z17w2`W?h5QYQq36!LPo6yaK-xxwx%~B)Ut$5lo&k@V ziBQ__sgfP#QJ3MO$EV<_OWz-_aVHs%1l_D6+TeQ9<>2Zf4>bK*#%!; zD6Ud|{`u$QkZ9lmA4bTm#|#DuYCFA7f_~nR5VT2k7`%@LN@0}mPX#dYabSf>ATEs#idk%& zCadl~EGxv2HfG%02gcom$wTKe@8TkGrdUxND_{qRHZfFFDSa*Pmhkv3rd1WCy{jd_L!Zo_i;_au7h$r~gYC>$ zftN3LrbOVG=1q?mxQ|=MyhhjCw$&Y@L3lGBF;?i$J?h4206#o<(V|5XI$4%>xqQ%S zm|}tVijQFdkKoF7TWv(iMt)CD+6p^%JSM-dR@2XId+bcP+7+8}y89YzAxf?ROCD0P z6~hY>XWpESv1+I?5I0mf(6S9^5p2l*NHUy7RN>9?0?bpBVRkIU35~rQ{5Vw1&CR1< z@5b`-w0?H*T0`+z$pn}kABVs2fYumR&lZwNqY0+yRC{>PJ$7SMNUE6I0pg=$5R}J= zp+&o;n%#)U$Eov7Feth!11(%xRZpLg?8tIPPEDhLGRWMJlhYuzag4Z=F2Cu&{`Id4 ztorb=OO03P5X~?s6L?s~n5HY7_Oj|67g(jfy#H!R^whmb)6!TT6AKM8k`UMqX$AF0~E#KtlK z@g0r4sBR!-GmhH!DzJ~+2~v2;31?5AjwSE&qyc#x1LQb$8`5aik7Rgf=v2`V2I9v8dO6Jw7f)SI|UO zWqAYd;D=a)Gps7NDnJ37j$?Mx;j1PA`=|t6JlJwX6~3`th=)3chHRLjfSbv`=L%qm zFvxO4@yB6=qDBKnty}QjL4#-3euWsEru8^?Te!V$7^_G?rX{#;-97T9I(Rph@o=sE zRvA+9z!7C%f(lxE+W!@x7}ya0a|{TP#WdCyUsgw4Cvlpml5EhlPQb;z9HF!v z!M_5NXB11yp}9B-3b}iS49)hT^x@f9`gU^iD!l%$kQSq(oz50+ca1!OeyYMT*$L|7 zgo2o?)M;0kq0R5;=rF{=ls`i4VF^Ka1&W|@ggjrd-dTMbYBo%7Q~_y8o`&UgjmTDW zYX@?+i_GOqm!5>0;Uq?;4gqf~rz@>X*0Z1w!pg0wSP$2bz8!bbXoZ5&uff0{DYuXZ z*UYUgdfxCtbR4P2-Iz$TT!uSY=|IA3P|0j8y}Vtosa>uQ%)8bzQ^s=<-Az82d;Af} zEM!pjEo@=Rv6(6`077T=B@T&V_7M~QQe0EElHIdNq0y(G+^loCj%;MQxg##M=~$ag z`p=!;KKGEm|JgmmG&Zl2@(QdZk$s^+qhj(W;Z$G9^T=qFIg7CrD`qGLqclsy!XUo8 z3SpZTiM6)u&Z|wrqX*@Hojtzigg2b z$rijW)n@19@GcwQ7#JigjY{Rzp?lyu`fUva1qGw?Z7-BW(es6{vzjZqa+`1gNJ z%fB8Oad7?qHLw$!Kh3)P@bfRfyb3hp>gCIq7aI>Ow*v_FC}jqDSN#PY_@Wx9j`3eH z#0p8a3S$ZQE`69 zRDO_pvTkoi1WwM-a%qBl`;PmQ;fh>Q7B9vD6dB~{oZYhBjBZ`1NT@@I2u zoMG|el%R`Yr3iV?A#DDvM}_bi=c$W{upB`Mq<*WcVYP_1QKpl_sa*pOEOCP6n*c>h zpu2IAb`e<1>FY7UYl%jitFHDAA=b8O7R9f5<_H`RjWmbiI*&A+?F9W*l@SpJHQr(M zX0+PQT4P}ADA*~AW2RX>cuKsuP^>1fxxsTArzXa~tf7BaAN?X^mZrSH`4Em1D)eY~ zm;jHV3N~NTU&l>Y>olqd9S3kz_earcKd+Nf=0d4bxn>-USh@fWDae#!tG1_1srHqX&Z8}>6<1ntOXg=auv%~B%{0t&{MF09*Rcn1GaeKEwn zsYq@&7&DNcmzbm;q(R{tXCT!{7}!_LdtLn!6WJT{1^&1Vq+VyL@>1dqA5_{e;VZfO z?Aw$~gK?A)=vr88Dj}vtWIN1daazP(4(tCgU1_Obw0JJrcGwvy=%ml z+>>1?qWyGt`+%Z0d=xMo8=iERWS(M~@);QtOWUAWc+#=O&!DhxSW%4~uF7%dmuapRVyyuAHsXAw!spMh09*qT4b$x6w) zU?h(jM_F%-oC?4$;AQnU(sWwsHwElaLV&&EN4tvyvcv*_YRfUV{Y zDK|0==Cw3}RqYs?9S4tsezv7O53yOZOhh}wwow&?5e!wXqp9T}KoTE><=*PosE0^d z2Z}cypBRr4tgk6Efi!U-pjD7aUCyjO|Hf-16!8v(d`@dj!i0)**VA3rkFWpKEM~N8 zRm?>XS~H;qX#yJ~E;a(_egZkM0lw!jv}g`)e;m-i^7}mR?1ztBi3thXuBRPyII<3p zE+SZ2RkaSgEg8{4iOmn=U|5K1Y+rs@MoitbN45tVVzBi?Df(!%(J*vp=xOwJR~wnoU=sr6 zul8~!Q;j!G!ch8Uo<(2fQ`TrEBxE!`^cGvL@9~X)L9RFC92q*GqaoH<(qY{%RpRmb z#eTGyrmLd&jiojZg#n{5(yov2Si%^yc6zs?_$7)KICOM`C$;3tv^5tcifM?Ov?Brx-G9y&x=F{(x~$ zr{$3=NsmL?Rs-@izW9V@E>H$_ayXo{)^P5GbO}$gqChp{qIm~N7{sWZf+E9(2|#Jk zbxbJD3s{xP(2sTpO#;6^yxYF>w;*n8>?cqxIH9;m!28~+7WEs(cM_K?LjPP39H;n% z{KOB`um5!flSyPaM5m^2)k=#3`JGghP7()lncvhkvr>0#HWHHH>o{=j;XvM)hVguF zDH^Lm=>WiE2Z=1un%2-@KOQ?3c0{a_EFVa-8I&NsY^GUB8=eja2(4{?6(E9ejh0Hc zU|A?PNNXr82KA{8qO}f|zdBI-m1wjg!g0ruWIq(v{ux8=I^s>deqkP}={o>?{L3@mOXr-Cvv%5_jeK{_!uRBLnUFM)rVMaK2f`t7mvnFoQP%wp zVlI!b2dwtUGgj5T0CbAk=9#Yb@AlGT%Ep z$4xFClK-+{vyMv%L%$<-YUer9$QhlhrPWpj_xbKO8_pH%QXkQ@W`x>Z8~oB+ZkzCs zOsbcgp?dPOQeos4;obvj4^EtVi2zY2t835BZ8(I2uGW?YrURM(EzCye)CNb6Zm|s zaU))x1(Hej(E3I3xieDM9=a^>8KZaCg+jeW7~4*a3jtR$>N^3y3+$Q-P+E zL&c4mEC}QE@{Eo(ofVXkX-sRHo>IHJA9K~IzUQ|$qIR75HmR9_sgYAE`x2}3F< z5kiTx_*yw2%B!Zksw@=4Z9*VmaTpPEp;SZkh*qp59}>< z%

^@#Eb?4CScpJlK+p%dHj}oqx|AVAR7iF%_t}obZ(~#zm8T!W_-;UCxaOum$=! zCg#|v>bzpRt|mcVjva;b#!6!|4DT>6@<<5kOuJqX%C^(*9=x*0Ww1qM=-i>OgSk`u zEXLk$f-5*rEYVQfx%px^>}F-Pua@zgR45aKT_$w_ ze$C&egZZ^NTO9Jj*aOQm#ASOeQxqmhO2@|Lk9wnTQ_i#7FatkXXH63npEP`7tt_Y2 zI0j)uxRdcK98SB0GrfB(8RY>+0_NB)IwY#JN_6_zVFTCpFDxc-ZsQYZbR;qyfK#F+ zuR7CNg+b+)-9nG}%%)D{z5b&3N&YXSxbM~5pL-H#K&ZbMz`S}rN}isyv&hrr*W7NW z0&RX~FlgWYS4ZV&KpUH~^QYK(u4{o?!Rpw79R<0y1rAT+y7Q(Lf+2m6(G!>-ICCh$ za0t%LnZXLcDtx9r5J!7erCYZ_nFhtQt=o-*Q1ClR83Pbey1e;h5@rX3f{w$qaA*|g zUge%$qDPZSRgqrGE)uzAfz8sQ=>q96Z&CI!M#a!k*P|46a2kJm5@6JxWdaP>e1xnx z=$F{mXT7*l@87pal?0(eB6G`z4d26;$Tq=)Cf-6NN+zwdpqwLB$o*q0hF@rY!$^LG z+4~7K%wp4kAyqb*`MnJhRUE+1t(!M1R$naD0ivHYOr{3FK{EgW;V4=?lvmZq4)3{L zgriR|4^Ly~FpHF;(qB8r_J$w-o_Ih!aR_I*qf14|9SM$l8w`DMJl>Z52M=;^2O0$4 zynAcs0bGdm*o3VEo}rtQS#ubPA*l;f@#-{kjuzjvH3CLHM}_T<#Hnop%raYz80Uzz zOJ+@H;IIDlu7^ADdos_NM6;g>Ad{trnaelOs+hh^N1R(DZuFb{^qXUJn=o;CjJdW% zsFY|j9TgPGC@9F{C9Wd7b3bdJ{&--7WVM5zIE|%B(nS*25yDTyCK%$zu|9WzJ=2Z4 z32JR&VK$TQg|Z&pf55vQLbFl_x51lO-@l&KRU(0xrusG{6Kz_b4EyQ7{n% z>gQpE3t99w!C_~%ac~6C^@k51E*^k05ev6c10*03td0^H7m(c%k}n744H?hi)ko9t z6=*0fL)67-@TW~il#TB*8>Pm_TcE zH+Y-YO|(Uz2o7MR^E&xb0B<5H7Ip$j055Lg#l+!4ueGK)%Vl01Ny8!|`e!%BLvaj` zGR^X$b>L%{g}9&dHEx&PWRWTC^hI@pTh#&_@xBL8<_c3oq6I7(0STV6?_S{e_F5q+~6^ zY^B*Y)N9V?b`z^f_)l&HxE2dQX3sN-Qyal)y~cS%R9HJ8d;3@=>SR}Ll>+{z^X$ve zX{)^EHsqy4gd2DU-@CV00=)@2-|EZ)!A@N~U^F(~VJ$b-GNIE5EAcoCM(E7OAm^*t z+uNVp1xVfm^+mV+T8O^x4^(}<&dYv|21tEWo7#?S5sl5U-Tx>CJi2kHO?KH`!o`7P zg=am%gqH&)FvDM_P0RaZ&~6GMy`JPHL<*VHC4mpUce>k%Np^NjVWKY&r~oaQ%4Qqo zgCkt-gOz(E!m`O&N^sk@^4=a{EIX|NSG(xQjqBI{0hh{VGug(_NX#Bq&Tw;HYIU!h|2!yY~53thTTgr*7{Zk@2} zY5CSW{Jvoqqf-a_EQj`6(ah)_NO8geYv=_pS+uAI1vv#xK+zO*jzCBWsPqI7UE)kp z*3_c497WRWgOOh0p-83!N#dwhB~b#LI|49vOnNDJ!79_(A~Jp)Gz%@knh;sg_5xXv zfYx#=^rues0TM*j($pprV;uU+g^Ly)Lr?xFSR{cyka~X_vITP(V+B~)9OyLM0KzCP z(*+T6iTO+Nyuw%@^i&711XBmsm2n^&x`D9B!7Pe8)WrZ6jHX44AAr}Yfk%Uay zEerIon;+n{VvE`j$Y~T;30eq(w=fD=x-kIJ)JX85Hv$ZVHWnd^0zV~an$*F^fwf;x zDT185$d&+Bk;fKi;(6HA3I z+~hFjeIno4s_;#<=;6VKtI|&L`1aL=G!MEKH|ESNKFz?>dFtP3Yo(rJ4 znET}|yD(()M~xrDb?JL~!Q3C||MJN&<-PLw!;dhY@Bbilpx^jE{gL4ys@6=>{Y4sY-Ly>&*r^gnyX zyyMqlCI$z-b=mvu%E%1}KSnI6^JYeXDa=FnlZ7HBP*9T`%_^CN47a6Jkrv}Gy@3m^ z5}S^j)b;yvFYzL=@W3R8B|876xt*c!|9aV8RaaQ2uK4!b>wquJ5Ce1hxEP%>$oj+_ z%}i8znPZe(SNf}URWKx3*T#)Ztfr=h_b&vGM|m zGX)?Rl5k7d8hf7Y3Wy{Z!MyL^sji?^A9+@dP1cQ@B_$<8Hh$(^&;{b576MZyi8&v!-yHhQLP!P^hv9YnuD|i?z+K8kL1VmE^ zwh;I6JhO#QbY7=CnahD7H@EhAkwxkZtRW~5>(Sae$nr8C9)oy$JBv^Y%78Sy9Ium| zjDGkQ(M$G@5XaFY{S#qjGmB;cv@^|v%APQCLgCOvC~0r8>~BO;Kne3o)i-2fUsLO* zi7i_gifTw`i7uhkafIBOhug$sV@ILLE32v!rQa#P1i5|>7+57Lk(hDNU%B+(+U@;r z5d299a{sAQ8~hJ3?B{iC@@~F?KV?Uu%rZyKn9KVOqm%Fa`Saw>eAbXBVcs#9tc`%vZlu*U6ZU^)QHfNC=N2fsi_X6Ta@WF zw*x@nj=Q?%kU|4fz!ZOgp*T-WSbMHMfEI(avII{9e}L;@4Z4M)w2S4sa0S$q@M&(ws7&{igiJw5O{>Q$qr!*G4V%M`+%#C1R74!ofPK_#h?Sk)*oTQ3NB_ZS%$T(;&7 zoPs$N{!ToKDGH6)XA~OufTt&j2IZ7Z&|&w;N2q-_v4F5=`)wRks2n_Z{^G@2tVe>0 z;(j>4jD8dxoD-094%HHXb^CYLArxy59NfRZl-qDm@@@KSP}3mFOG4pDdYw?uu}IHJ z7627Bkjlm}| zyhdS6-Xgg_MX>Zt#IpcYMTWk3$D|X&9czFpwa-~%O@IKxi_)or zuL>TZ&82I>gllb*PJPmD9GTsg*inU0SDhqL{1VO-WP3q*75o-y&0%nbX6qVxIDiqR zK015FP{ojUK6(anXF%~E3ldX!v3Sdd}`x>x8_?4S0q&~t>%R`p2{YO zG$XCUNlZZ2D%nLk^Yu#^d-?03IN4YV%%dSHIy!Hg4zmA7IuA;)W)w%?1a85w%%#p( zboO>P+Mvb@h1l7u&4;I6kB&-`$gpM?XPSfaY@ zOz{WV!_A6DToK&8yQy6s2v1IpRp&!^Zdlc}V#EYA)y#{XStL6^bjY`a0KiO8TDqaj z6m@$tBA#8-0dzoa7^IJ=8-M*;OaO&VUV}fMY;f{LWH)>`-l^c0Ev3fg5hJ(xAXbP9&0D#i?p32uUR*89y$P0&1yXe)CiAN)9 zqx)B4R6EQj+A@31c%elEvo0MN^e=~w<08X(m8k}osv%}94vjrjkUk~Seg+l z#0iO^oK!=vB%YnU_8b_KUBkI)nTsg+uAtV+>#s3M55*Z@_j`4#J^QuOjLt7uvc#oH z9p9ttSyf)i`^S~F4oz4Cn`$P3x=8>DwqNtr^9n)~Yp_jCP}5yx*5J-m%JRc_DJg70 z+TDU4k#tYx;QbC^A`-`8W{nzau<3}rw3S{6T8ALMt^hk@4$R!t^WE!%=@7=G+qQo{ z0xT&MHGTqV+GQ;7dsfrT*2YqrT%a?gOpq$OF_N$oiqzx<-+YrP=%DcK)xLAiL?Ir! ziZ-mA(6)4xGIJm4#8D@O*HyYn`iwWQsMMp!r7ec8R%BmuBw&%eQZi=dRsAe%r`aiH zC=!dYW(X7sgUWwGL1qP?Vk7t)Sdkb6C31Dd;(l-`V3kWBt7_ZFLRlXT1)v~=V~b(Q z{87S#3#{CxnR2gJ-r=vAD6$arWzzAUR{%R+fe}}_QRy-a@e3rVpnGcs5O+Td_`6{c zG23WGBw>{;4#Zaq1aD@h|LwIxH%P~N5e>ZVi0~LoEJzKsqU=3FIx3Kn}?nS~_JtmtB2?#_uF(y?QQiV1ANDT>3 zT)t|RNyO(em}EV7W#Di@6kk2~5o=T<#`L$4{EP{W5(^e*l)jTP^-9JV)baQGQ*ncDl9GsOx=Ms{9 zZF<43gK}b~DTlcr0R8jr+qYNHzFt9c4Fd}ph7?>1x@cAmnK}JYu2ORJM04AWNst2U z2%@{@b$WpcO_^z1f>V2p8=;5LHU=Bs;mE1p&VAVkxyBqPXGxGy3uj}gk~Rziq7@@J zrtk`IQ8%W{)Bm}qsoDdyg>DzZeVv{Bz@LB7;^dy+UnFn`BScrTS8mgM?8`0oXL+PT z8;Q#j4T_$<1L!CYjroapzTZ=`5b`!PMn4$1|Pd(ZbK60 zKyhjqwRUyvSZ_@U!61`d6VOP6;WcPk!S!&~F~_Q=P73?!mtTIlg7z?!)FVIx|FDFl z1BcEuLO=Lq>o^G4-K~ckLpMS$3arko_71W)9m!^a65ZNk2!2B)Nz>X830mkd-YP&Q z(P>7BO|l{1fnD4RZ3au^pn*xpix(zsgS9=eFqsI^6_xbcA3>A6< zjcDEr5nAFhF#BIm(DBYZbw%h*>n2gk=J|4f-Sm1E!8F9F)|?bBmxr)gE*X)92!~l+ zu^=8GF2^qhLKFFYYY78%`hJK&{WZ-81w1;3JNIkbE3)ATBaV4H=LxM=yOEWDQ1`Am zh;HXl$iw_bh@H$JmNlBT0-d#M&Io?w>9NCF1JX)IkQWPZU9axcyEOaY7RBJah|O z#rA#QWdi;ndn`A7xmM^VAGuop=~6|P93?I{^U5D1q3|NQ#Yd=wVg~qEUnZTerDB-f zmQA`E&d6sEXCd^lV1|OU%q!T)9Qae*%WHP=>R{TCO4oDDNs6lrZFKO93SIG&wA0$ z$O(Vfx%KE5uENx+W`N7roJ-&%GmsKnz+X2n_%)*n7$s*hr$2DtwY^B^mRjdnk0G%6^=!8WZRPy7Go$W zlG2iwsWRk*g*%L!&Y#2UZdEhTVsU)~d5WSqBAWz=T05;VY~ED)`xPvksm>!%2k8oC z_8$&zI~(&7^Q@*(Yc(3Wqr+9P1avDZTh;@`g)?U9nlez!A=|RslW9!3<<9%nB7_^c zD$yY1LpbBLvmBURNebDHdX;k<0uwxy$=Rm$(&i!V^R7Yxh$+hU$Pjw$~kF)nd;gQ7_ol}F&paU=(bs5?J*;vm4AYu`Mp}s7y zrAJK`Ohf#qvwBehI%d2euc8*Hi))-`kQ8;gD63gV2z8VBs5pD%U{kZQvTLq{%NGR~ z{Tb_)khUh1cj?mhRA(?@FJFCe*^FjwG|Omov}Cf-_4Q4nji~Q|w;?(Hpe#xnB>|OI z5eif&6YTQjMFt_1d^F!r=m^}j{0af4vZH{K-FEMN1I#UHb#OM8$kuEm zOvlk0&+*aG6C^iC(t%YR0^m9pI(3)m)C{X;b#?{}%0fg#=-{wuE8|+sX(Wd1kQIkU z*TN0&ueS~pDAn4YI4UPRYw3y=QQ$O`Fl@k13@K`BvDVbQQWjDP!d4Bvqhp?a-s_v1 zEb-#gIJF2eyQrws;11YP*j_r#&qC4Zpb29s&CaggTlwgbYv?Zici1hxemycLv2NK> z;A)OwtYUlFGuQ>z4WmW~4|WG|K@L|j96dgt8u=-FzwX$jw)acrB(!pncEvJ#ah6pA zb&R(s{a`iuQ6eVkPfru5jmfARO8c`l{=&3)@R$oB@g8vWXp*buzJ*PY>wAX=9K}2K zJl_M^#+)Tu=(m~#K2)%aOKKEvhcgvVH zzeaS}4RLqy(q*L!78T5KxAhjE&%~k9I+mair=Og$NV4UcLc5u0`PSUwo?*PS_wx52g!{~S1k6SobDUL5RD ztD>NxPDu0i`sTwwV;C;Nx)ouQ+4}Z-45`Y548Q>L&y%vKcgZDh&J`KBg{>_AL2~i) zhz49?BHc*fAzB_Ab`-c5w6}+v&YQW+Br{wOs27)I%$C7^oLYCn!9CEqH5IyNJBgPp z2cdlfi9v~t34-w6)I9?~{L(pTAa>}ryqNE+ChAGL*%Y$@R!#&hOY)jVg1pLGP z*JkvV$+tW_y~F9zIMMKWnLYMU;q93{lF25fgWvM;S&1}9wmgalp9#CZ?=YGVBZ*j) zHn%{HYL3L>QERW3qU8qQqr7H1r);^yxiDi>em99J=1y8lsBdpz7Q=v@fnuHW{papx ztXm|uK?=h5sMtr_9yQ^dGwLp@t3;1N#phqrg4!*HV{e}PLIseLq&!-sT{Sak##;KS zYE3WC*ysnq$R9YL%G9CXpPYznGei%M8HaBHw|wqQ^~J#4V(#oycG-6_@_op( z5-9X-opJcNz||}$G~eYE`MtBNTUUOy&eYhr0KIt(84v&>*$h1*ajP`AA$j`oUg($7+Z;|MS>()T@yhGGvfIc@6;7y66Dh*qK#bqZ1cz0+6s-76@1O;Br7(pvgEt{ z-TNg>${(XN+s2}qR2+1@A3IbDQ(H$6tDR1a0G}ft1lDlF=MABt$>`)HvZ_Ws6b(4N z57Tx>aaa`{8HNS_5|taBJNpzZx}%eahes_uA|RYp#AZ4M&;`hU;^#-S?Kgwq$8SGI z!%i-<7o?BPhVpeMBK+Dd)ZZ z`VZ*;l-P74>|md$Y5$$*tl8N`c*jl!IIMd$=>sYvWHnHq)S>5x1)dD42!Wdt>VVNT z54tMA&IPxx^!pEx?t&2epCQ0Cb81im@33iIf_#mEV9E zF+&>KE)eqK5s<+k42K~>1eFj{0NGPthOUW!m|byThq-e%S0h~zsnq5USRiLTl1$Lp zBcMi$L@#hCZy?P+R-IIF+gd}xW!Y56VnhTzY8DW&H$y^zja7p@XPT7m^)x(vQ5md` zc0#61d?_MY9bFQ(B#8szPjr*Sfq*nfy$qen$t(fUp+5Kafolm3sVS^rR#fV2e&IJ^R{jq;F*C6>^mCU?TBnXh6XtXRe`KtWjaxLyGB4BT%F7c8Gp8L|9BiUt+hR10g#da3@!2 zUKX6J)=*jL0vSmSyJ#w5TyoJvv}O$zqIjzz&nU>j$Y->E=xzBR?;m$Pah$+4c`rt% zlbkx&-fJcb4}0Dyr|%)}R<^a=fr{FV-}Og^gsnx<6ivgpP}iiYep?L3P1Fu$N5;Cx zxo$_Om_a|5CmdS6Y(G3u>o5tHWRLv^0{zH2=Yc!rm5+i{a!*3QxxL;7;zn&~OFZ+S zl&ppBj%n27em1-j!SlW|8a!C9e#5H9deGAk^>mO+mK!OTz?;X}ShXlr6cC;JfS6up zBI@2BNJy>b7udX|RU&s@=eEPXA;=j|Ncs*EU8gF!4cYzNSbtk+hZ1f+DZvA}!k4MC zBI2@K%>rv}O^v|D_YH6ppN34TR(}R`Ov#lZ-aMb5~#x*rMn9^uT)tA2KgfSFeI{s_GHV)ap zDKw#%LKBm_??R?FowcWUmz911a6#+xha!y;8#`l0zS^q~dw{*yFy11=c`Z12t+P_N z+S!RxGvE!sET%3_%U~2eA^39p>)mFeHbI^CMP!1C8isAHo79RKNJmahQz586 z_#SaGBEV1gg0EApZ^j7q1r*Pstg8V3N&OGyH!4ZlOWf=LMFd3S5&;L$5EPDq7MJ9r z`+1JE+syt0+l(`1h3|RK@A5p~+f&7*M3a(B47zIY`zGJRwa;VA!Z(#4P*oH~()D{r z4@{03NT)=BN;^!Blgd@8%!+qFTGL*y>71=WL$#j9#F^)CTL=2kKg>2x6*AVnV*I@n zLh1C(5w`TT`IZiA-+Q6&QbYarvS@>YDtjLAjogJso8u@gT=t-qOzc@mn7e zpoWhPoQ~Q~?-u1T~=lONH!kiszU)jU_PE(;kvc zmp>q^47mYs=g!;lOOQ-;y6|NDr^yrk-le%bcq`)R0DC2ae#UpBIX4Up-^aZ^$_v50 z2SR-&oSOX#6c{@(P!rcQ?vtv#B+4rIS&5HtD{?AD;j^AFup)n3JYlY3FP$GW zUrWvP!{bdI+|wH(G_GoP;$VMLcI1tMv{pl#sk28y0_dFAIgYwpp3viR5-@x-{jd}> zr%Zny?bq2XY$>$STCad{rly}cS1Ig#@Tx^F}XYU?!yR2S#EM7A0~E zC36!eVRuNT0e(xVmy7j_M|; z-b2l>AHl|-B0T^@+urs_McKTxonx#EFzG;DhF%!#L3L1YF$Fln8B_^=gs8~;KN-dN<=J|rnpx9} z_{q4IF=NO2q8{^SX_y;iq=sW$FfS^nDJq8;ZG$q)m(LIsxLjC!}>@ipAvR-LBj&p>?W#+4b_NF&jUYx zF5Y?2`;_ezr+Q`(I@iB8`*dkmdG$c?!9l_L*X%?~kW7(rRJ@kYt)U!ZLt83KC_S3) zE$q5>Y|_k-HT}>1xFf$r{@yJX?wRK3e;x*ZIc8ePfK7Hh>rV zXM*YjL?S@l2-s2-5S%^E_eEQX}wv1y6a9uJjRzFc%if32PNdb1jo21o#y?)nG znrjn^L7L2jxDD*?Lnl+pijW3{rH6cKMFpI*%HGaSTln~!*VM>ig@~55H)~B!vCLBc zPMhU=V&Tz1#g%%A(|iB{pA7!GT5)piO{beD**2B9{zDaYA3bZ+=AUEv+}+|dD*ta& ze@sJ(>oUf)=v34E%-j~qHaMiCjGpFMmWV7#&Tug*yJY}Dmfu<8qS!;(>Az*W<c-f&m8 z0^M8F4wtO3|5Xk8-cp}xhI%;ji5uEAycLu7`V%=`bVo!ZrdRfH;H$+$Y+o>_9PMt8 znb$%H+{k*rRc6e%+6dR~ zgDgZE!v{EIA8hx3-qAk7bK3($o4B{7=f<&X*!*bnn+A>dctY1OJ274{={j`Hv)0b| z%xJV4o!PV9vyJA^?B@zF*hdvr#7>3PDR?jUfB;!5)4)iws(k0WPfB-acj-XQDC$H0tz-@M5sTkVV~nc(VeYkgjA9{TDD)a zouasRIBee}Ch``Fk~T`*Z3P|V+WJ=081!drtz#YqhZWj!go=_zJhSXX6%G2?<^UiF z8Cxv_6=@1olzH>2j0o@Lg6m=$!x=uks7hmFbzhB@d5Q=VtTi|@XewF zl8*;*E_z>hFHz#Mrk3}9_VvJw0{tm_>Cz=JlSoVnNV}JO72W=g60U zyC8Jy=Km21pCUEk3z|ABl<5v%4y5p__QL*;~L>Ia*}x` zBJ~APy%11ZX=mdeyp#)zY?(>75-p^kUqMnZqi!dHZwqeBdq+QV0NsGh>rzMB#mST( zkyJ8`rGn=2LlnV(-Lt0@=N3V{XFSfOiZ`0Xe~xpN&9hPTwxds(Hg||omYRk^AS*X< z+kkscLQ^J^D`95oo9PwJMjLIum&w%fLj&VEtGJd{l*sW7a8L}77zH=0+HoBJYVCb z!VMT+HRix8?|2O_9*K5Q0{W`KXTeyeAkZcJZT=JNPX>jFx74MNKMJdebTrVKgg?*^ z>kNNWKQa#?zzRB$8p@#Cw!1|9%ow|%|BSISKo}kFWA)!`%KX7r#eBb+%e*;l}0672v literal 0 HcmV?d00001 diff --git a/code/it_support_qa/results/contrast.svg b/code/it_support_qa/results/contrast.svg new file mode 100644 index 0000000..0213d95 --- /dev/null +++ b/code/it_support_qa/results/contrast.svg @@ -0,0 +1,301 @@ + + + + + + + + 2026-06-05T05:03:21.627567 + image/svg+xml + + + Matplotlib v3.10.9, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Internal tool named + (in answer body) + + + + + + + + + + House format + (steps + escalation) + + + + + + + + + + Token-F1 + vs reference + + + + + + + + + + + + + + + 0.0 + + + + + + + + + + 0.2 + + + + + + + + + + 0.4 + + + + + + + + + + 0.6 + + + + + + + + + + 0.8 + + + + + + + + + + 1.0 + + + + score (0 to 1) + + + + + + + + + + 0.00 + + + 0.00 + + + 0.20 + + + 0.00 + + + 0.00 + + + 0.23 + + + 0.50 + + + 0.83 + + + 0.58 + + + + + + + base + + + + + + base+prompt + + + + + + adapter + + + + + + + + + + diff --git a/code/it_support_qa/results/results.json b/code/it_support_qa/results/results.json new file mode 100644 index 0000000..40c0a0d --- /dev/null +++ b/code/it_support_qa/results/results.json @@ -0,0 +1,20 @@ +{ + "base": { + "internal_tool_in_body": 0.0, + "house_format": 0.0, + "token_f1": 0.198, + "n": 6 + }, + "base+prompt": { + "internal_tool_in_body": 0.0, + "house_format": 0.0, + "token_f1": 0.233, + "n": 6 + }, + "adapter": { + "internal_tool_in_body": 0.5, + "house_format": 0.833, + "token_f1": 0.576, + "n": 6 + } +} \ No newline at end of file diff --git a/code/it_support_qa/results/sample_outputs.json b/code/it_support_qa/results/sample_outputs.json new file mode 100644 index 0000000..f2643bc --- /dev/null +++ b/code/it_support_qa/results/sample_outputs.json @@ -0,0 +1,16 @@ +{ + "1": { + "q": "Internal sites are slow to load but the public internet is fine.", + "ref": "Slow internal-only traffic usually means the VPN split-tunnel is misrouting.\nSteps:\n1. Disconnect and reconnect GlobalConnect VPN on the Corporate profile.\n2. Confirm you are on Corporate, not the Backup profile.\n3. Flush DNS (ipconfig /flushdns on Windows) and retry.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", + "base": "If internal sites are slow to load but the public internet is fine, this is typically a network or internal infrastructure issue. Here\u2019s a structured troubleshooting approach to identify and resolve the problem:\n\n---\n\n### \ud83d\udd0d 1. **Check Internal Network Connectivity**\n- **Ping internal servers**: From a client device, ping internal IP addresses (e.g., `192.168.1.10` or `10.0.0.5`).\n - If pings are slow or fail, the issue is likely in the internal network.\n- **Check latency and packet loss**: High latency or packet loss indicates network congestion or hardware issues.\n\n---\n\n### \ud83d\udce1 2. **Verify DNS Resolution for Internal Sites**\n- **Test DNS resolution**:\n - Use `nslookup` or `ping` on internal domain names (e.g., `intranet.contoso.com`).\n - If DNS fails or returns slow responses, the issue is DNS-related.\n- **Check DNS server performance**:\n - Is the internal DNS server (e.g.,", + "base+prompt": "The issue is likely related to internal network configuration, such as DNS, firewall rules, or network policies restricting access to internal sites. \nSteps: \n1. Check if internal DNS resolution is working by testing with `nslookup` or `ping` to internal servers. \n2. Verify firewall or ACL rules that might be blocking or throttling internal traffic. \n3. Test connectivity using `tracert` or `ping` to identify where latency occurs. \nIf the issue persists, contact your network administrator or IT support team for deeper diagnostics.", + "adapter": "Slow internal access usually means DNS or network policy issues.\nSteps:\n1. Flush DNS and restart your machine.\n2. Check if the internal site resolves in your browser's address bar.\n3. If it still fails, open a ticket with IT and provide the URL and your department.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help." + }, + "3": { + "q": "How do I install the latest Node LTS on my dev machine?", + "ref": "Use the Contoso Software Center; manual installers are blocked by policy on StandardBuild.\nSteps:\n1. Open Software Center from the Start menu.\n2. Search for Node LTS (Approved) and click Install.\n3. Reopen your terminal so the PATH refreshes.\nNeed a version that is not listed? Still stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help.", + "base": "To install the latest **Node.js LTS** on your dev machine, follow these steps based on your operating system:\n\n---\n\n### \u2705 **Option 1: Using the Official Node.js Installer (Recommended for most users)**\n\n1. **Visit the official Node.js download page**: \n \ud83d\udd17 [https://nodejs.org](https://nodejs.org)\n\n2. **Download the LTS version**:\n - On the homepage, look for the **\"LTS\"** version (e.g., Node.js 20.x or 18.x \u2014 the latest LTS).\n - Click the **\"Download\"** button for your OS (Windows, macOS, or Linux).\n\n3. **Install**:\n - Run the installer.\n - Follow the on-screen prompts.\n - Choose default settings (or customize if needed).\n - Make sure to **add Node.js to your system PATH** during installation (this is usually done automatically).\n\n4. **Verify the installation**:\n Open a terminal (or Command Prompt / PowerShell) and run:\n ```bash", + "base+prompt": "Install the latest Node.js LTS version using the official installer for your operating system. \nSteps: \n1. Visit https://nodejs.org and navigate to the \"LTS\" release section. \n2. Download the appropriate installer for your OS (Windows, macOS, or Linux). \n3. Run the installer and follow the on-screen prompts to complete the installation. \n4. Verify the installation by opening a terminal and running `node -v` and `npm -v`. \nIf it does not work, escalate to your IT support team or the Node.js community forums.", + "adapter": "Use the Software Center or run the installer from the Software Hub.\nSteps:\n1. Open Software Center and search for \"Node.js LTS.\"\n2. Click Install and wait for the package to finish.\nStill stuck? Open a ticket in ServiceNow under \"IT Support\" (set P1 if a shared service is down) and drop a note in #it-help." + } +} \ No newline at end of file diff --git a/code/it_support_qa/topup_hardware.py b/code/it_support_qa/topup_hardware.py new file mode 100644 index 0000000..0e50fba --- /dev/null +++ b/code/it_support_qa/topup_hardware.py @@ -0,0 +1,62 @@ +"""Top up a single thin category (default: hardware) and append to the expanded +training set, without regenerating the other categories. + +Hardware answers (short requests like a charger or a cable) resist the house +format, so the gate rejects most. We overgenerate with a retry loop, dedupe +against what already exists, and append until we hit the target. + +Run from code/: python3 -m it_support_qa.topup_hardware --cat hardware --add 12 +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from it_support_qa.expand_seed import (load_seed, gen_for_category, passes_gate, + to_chatml, pick_teacher) + +HERE = Path(__file__).parent +TRAIN = HERE / "data" / "expanded" / "train.jsonl" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--cat", default="hardware") + ap.add_argument("--add", type=int, default=12) + ap.add_argument("--max-rounds", type=int, default=8) + ap.add_argument("--model", default=None) + args = ap.parse_args() + import random + rng = random.Random(7) + + rows = [json.loads(l) for l in open(TRAIN) if l.strip()] + existing_q = {next(m["content"] for m in r["messages"] if m["role"] == "user").strip().lower() + for r in rows} + before = sum(1 for r in rows if r["category"] == args.cat) + anchors = load_seed()[args.cat] + model = args.model or pick_teacher() + print(f"teacher: {model} | {args.cat} before: {before} | target +{args.add}") + + new, seen, rounds = [], set(existing_q), 0 + while len(new) < args.add and rounds < args.max_rounds: + need = args.add - len(new) + for c in gen_for_category(args.cat, anchors, min(need * 4, 40), model, rng): + k = c["question"].strip().lower() + if k not in seen and passes_gate(c): + seen.add(k) + new.append(c) + rounds += 1 + print(f" round {rounds}: {len(new)}/{args.add} new {args.cat} so far") + + rows += [to_chatml(c, "synthetic") for c in new[:args.add]] + rng.shuffle(rows) + with open(TRAIN, "w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + after = sum(1 for r in rows if r["category"] == args.cat) + print(f"{args.cat}: {before} -> {after} | train total: {len(rows)}") + + +if __name__ == "__main__": + main()