Skip to content

Commit 1fe299a

Browse files
adridaclaude
andauthored
feat(fit): light-themed report + lean-by-default zoo (--trees opt-in) (#52)
- Report rebrand to the Tracer light theme (Manrope, dots logo, brand-colour donut, light Sankey); drop 'audit' wording across report + docs. - Tree surrogates (dt/rf/et/gbt) are now OFF by default in 'tracer fit'; add '--trees' to opt them in and '--skip' to drop named candidates. The lean linear+MLP zoo keeps fits fast; trees help hard high-class tasks (Banking77). - Trace loader accepts the same key aliases as the scanner (input/query/text/ prompt/question and teacher/teacher_output/label/intent/output/answer), so middleware dumps load without reshaping. - Docs: document --trees/--skip + aliases, note the Banking77 benchmark uses --trees, fix a stale tracer.embed example. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 36abcfb commit 1fe299a

10 files changed

Lines changed: 94 additions & 32 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ User query → [Embedder] → [ML Surrogate] → [Acceptor Gate]
9595
(traditional ML)
9696
```
9797

98-
The surrogate is **not another LLM** - it is a classical ML or shallow DL model (the model zoo includes logistic regression, SGD, LightGBM, random forests, and small feed-forward nets). This is what makes the cost reduction real: inference is CPU-bound, sub-millisecond, and free.
98+
The surrogate is **not another LLM** - it is a classical ML or shallow DL model. By default the zoo is lightweight and fast (logistic regression, SGD, and small feed-forward nets); the tree-based models (decision tree, random forest, extra-trees, gradient boosting) are heavier and opt-in with `tracer fit --trees`. This is what makes the cost reduction real: inference is CPU-bound, sub-millisecond, and free.
9999

100100
1. **Fit** - train a suite of candidate surrogates on your LLM's classification traces; select the best via cross-validated teacher agreement
101101
2. **Gate** - attach a learned acceptor that estimates, per-input, whether the surrogate will agree with the teacher
@@ -111,6 +111,8 @@ The surrogate is **not another LLM** - it is a classical ML or shallow DL model
111111
| End-to-end accuracy | 96.4% |
112112
| **Annual savings** (10k queries/day) | **$302,850** |
113113

114+
_Banking77 is a 77-class task; the tree models help here, so these numbers are with `tracer fit --trees`. The lightweight default (linear + MLP) is faster and enough for most tasks._
115+
114116
## Continual learning flywheel
115117

116118
TRACER is not a one-shot fit. Every deferred input that reaches the LLM produces a new labeled trace, which feeds back into the next refit. As the surrogate sees more of the input distribution, its coverage grows - meaning fewer LLM calls, which in turn cost less, while the quality guarantee holds at every iteration.
@@ -155,7 +157,7 @@ X = tracer.embed(texts) # default: all-MiniLM-L6-v2 (384-dim)
155157
| `tracer demo` | Zero-setup demo on real data |
156158
| `tracer fit traces.jsonl --target 0.95` | Fit a routing policy |
157159
| `tracer update new_traces.jsonl` | Refit with new traces |
158-
| `tracer report-html` | Open the HTML audit report |
160+
| `tracer report-html` | Open the HTML report |
159161
| `tracer serve .tracer --port 8000` | HTTP prediction server |
160162

161163
## What's in `.tracer/`
@@ -166,7 +168,7 @@ X = tracer.embed(texts) # default: all-MiniLM-L6-v2 (384-dim)
166168
| `pipeline.joblib` | Surrogate + acceptor + calibrated thresholds |
167169
| `frontier.json` | All candidates at each quality target |
168170
| `qualitative_report.json` | Per-label slices, boundary pairs, examples |
169-
| `report.html` | Visual audit report |
171+
| `report.html` | Visual HTML report |
170172

171173
## Install
172174

‎docs/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ np.save("embeddings.npy", X)
349349

350350
## `tracer.generate_html_report()`
351351

352-
Generate a self-contained HTML audit report from a `.tracer/` directory.
352+
Generate a self-contained HTML report from a `.tracer/` directory.
353353

354354
```python
355355
tracer.generate_html_report(

‎docs/cli.md‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ tracer <command> [options]
1212
| `tracer fit` | Fit a routing policy from your traces |
1313
| `tracer update` | Refit with new traces (continual learning) |
1414
| `tracer report` | Print the policy manifest as JSON |
15-
| `tracer report-html` | Generate an HTML audit report and open it |
15+
| `tracer report-html` | Generate an HTML report and open it |
1616
| `tracer sankey` | Generate a Sankey routing flow diagram |
1717
| `tracer serve` | Start a prediction HTTP server |
1818

@@ -57,7 +57,7 @@ tracer-demo-output/
5757
Fit a routing policy from a JSONL traces file.
5858

5959
```bash
60-
tracer fit <traces> [--artifact-dir <dir>] [--target <float>]
60+
tracer fit <traces> [--artifact-dir <dir>] [--target <float>] [--trees] [--skip <models>]
6161
```
6262

6363
**Arguments:**
@@ -67,13 +67,22 @@ tracer fit <traces> [--artifact-dir <dir>] [--target <float>]
6767
| `traces` | required | Path to traces JSONL file |
6868
| `--artifact-dir` | `.tracer` | Where to save artifacts |
6969
| `--target` | `0.90` | Target teacher agreement (0.0–1.0) |
70+
| `--trees` | off | Add the tree-based surrogates (decision tree, random forest, extra-trees, gradient boosting). **Off by default** — they are slower and the linear + MLP heads are usually enough; turn them on for hard, high-class-count tasks. |
71+
| `--skip` | none | Comma-separated surrogate models to drop from the zoo (e.g. `mlp_1h,mlp_2h`). |
72+
73+
The default zoo is lightweight (logistic regression, SGD, small MLPs) so a fit is fast. `--trees` adds the heavier tree models, which can lift coverage on difficult datasets (e.g. Banking77's 77 classes) at the cost of fit time.
74+
75+
Trace files accept common key aliases: the input can be any of `input, query, text, prompt, question` and the label any of `teacher, teacher_output, label, intent, output, answer`.
7076

7177
**Examples:**
7278

7379
```bash
74-
# Basic fit with 90% parity target
80+
# Basic fit with 90% parity target (lightweight zoo)
7581
tracer fit traces.jsonl
7682

83+
# Add tree models for a hard, high-class-count task
84+
tracer fit traces.jsonl --trees
85+
7786
# Stricter target, custom artifact dir
7887
tracer fit traces.jsonl --target 0.95 --artifact-dir my-policy
7988

@@ -168,7 +177,7 @@ tracer report [<artifact-dir>]
168177

169178
## `tracer report-html`
170179

171-
Generate a self-contained HTML audit report and open it in the browser.
180+
Generate a self-contained HTML report and open it in the browser.
172181

173182
```bash
174183
tracer report-html [<artifact-dir>] [--output <path>] [--no-open]
@@ -192,7 +201,7 @@ tracer report-html
192201
tracer report-html --no-open
193202

194203
# Save to specific path
195-
tracer report-html --output /var/www/tracer-audit.html --no-open
204+
tracer report-html --output /var/www/tracer-report.html --no-open
196205

197206
# Different artifact dir
198207
tracer report-html my-policy

‎docs/cold_start.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ import tracer
7171
import numpy as np
7272

7373
# Option A: sentence-transformers (local)
74-
embedder = tracer.embed.from_sentence_transformers("all-MiniLM-L6-v2")
75-
X = embedder.encode([t["input"] for t in traces])
74+
X = tracer.embed([t["input"] for t in traces], model="all-MiniLM-L6-v2")
7675
np.save("traces.npy", X)
7776

7877
# Option B: precomputed from your own embedding model

‎docs/concepts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Coverage grows monotonically as more traces accumulate. Easy intents (tight embe
134134

135135
---
136136

137-
## Qualitative audit
137+
## Qualitative report
138138

139139
Every fit produces a structured explanation of the routing policy. The slice and boundary-pair design is informed by Slice Finder (Chung et al., [ICDE 2019](https://ieeexplore.ieee.org/document/8731353)) and the contrastive explanation literature (Miller, [AIJ 2019](https://www.sciencedirect.com/science/article/pii/S0004370217301126)).
140140

‎src/tracer/analysis/html_report.py‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Generate a self-contained HTML audit report from a .tracer/ artifact directory."""
1+
"""Generate a self-contained HTML report from a .tracer/ artifact directory."""
22

33
from __future__ import annotations
44

@@ -131,6 +131,30 @@
131131
.footer { margin-top: 48px; text-align: center; font-size: .78rem; color: #484f58; }
132132
.footer a { color: #58a6ff; text-decoration: none; }
133133
.footer a:hover { text-decoration: underline; }
134+
135+
/* ── Tracer brand theme (light), overrides the dark defaults above ── */
136+
:root{--ink:#1c1917;--muted:#6b7280;--line:#e7e5e4;--soft:#f5f5f4;--green:#16a34a;--sky:#0ea5e9;--orange:#f97316;--red:#dc2626;--purple:#7c3aed}
137+
body{font-family:'Manrope',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#fff;color:var(--ink)}
138+
.logo{color:var(--ink);font-weight:800;letter-spacing:-0.01em;text-transform:none}
139+
.dots i{display:inline-block;width:11px;height:11px;border-radius:50%;margin-right:5px}
140+
.bar-sep{color:var(--line)} .crumb{color:var(--muted);font-weight:600;font-size:.875rem}
141+
h1{color:var(--ink)} .subtitle{color:var(--muted);font-family:'JetBrains Mono',monospace}
142+
.card{background:#fff;border-color:var(--line);box-shadow:0 1px 2px rgba(0,0,0,.04)}
143+
.card.green::before{background:var(--green)} .card.blue::before{background:var(--sky)} .card.purple::before{background:var(--purple)} .card.yellow::before{background:var(--orange)}
144+
.card-label{color:var(--muted)} .card-value{color:var(--ink)} .card-sub{color:var(--muted)}
145+
.card.green .card-value{color:var(--green)} .card.blue .card-value{color:var(--sky)} .card.purple .card-value{color:var(--purple)} .card.yellow .card-value{color:var(--orange)}
146+
.coverage-wrap,.section{background:#fff;border-color:var(--line);box-shadow:0 1px 2px rgba(0,0,0,.04)}
147+
.ring-stats h3,h2{color:var(--muted)} .ring-row{border-color:var(--line)} .ring-row-label{color:var(--ink)} .ring-row-val{color:var(--ink)}
148+
th{color:var(--muted);border-color:var(--line)} td{color:var(--ink);border-color:var(--line)} tr:hover td{background:var(--soft)}
149+
.bar-bg{background:var(--soft)} .bar-high{background:var(--green)} .bar-mid{background:var(--orange)} .bar-low{background:var(--red)}
150+
.b-green{background:#16a34a14;color:var(--green);border:1px solid #16a34a55} .b-blue{background:#0ea5e914;color:var(--sky);border:1px solid #0ea5e955} .b-purple{background:#7c3aed14;color:var(--purple);border:1px solid #7c3aed55} .b-gray{background:var(--soft);color:var(--muted);border:1px solid var(--line)}
151+
.search{background:#fff;border-color:var(--line);color:var(--ink);font-family:inherit} .search:focus{border-color:var(--sky)} .filter-count{color:var(--muted)}
152+
.pair{background:var(--soft);border-color:var(--line)} .pair-intent{color:var(--muted)} .pair-text{color:var(--ink)} .pair-score{color:var(--muted)}
153+
.pt-local{background:#16a34a14;color:var(--green);border:1px solid #16a34a55} .pt-deferred{background:#dc262614;color:var(--red);border:1px solid #dc262655}
154+
.ex-col h3{color:var(--muted)} .ex-item{background:var(--soft);border-color:var(--line)} .ex-item.handled{border-left:3px solid var(--green)} .ex-item.deferred{border-left:3px solid var(--red)}
155+
.ex-text{color:var(--ink)} .ex-label{background:#fff;border:1px solid var(--line);color:var(--muted)} .ex-score{color:var(--muted)}
156+
.delta-pos{color:var(--green)} .delta-neg{color:var(--red)} .delta-zero{color:var(--muted)} .delta-bar-bg{background:var(--soft)} .delta-bar-fill{background:var(--green)}
157+
.footer{color:var(--muted)} .footer a{color:var(--sky)}
134158
"""
135159

136160
_JS = """
@@ -175,7 +199,7 @@ def generate_html_report(
175199
artifact_dir: Union[str, Path],
176200
output_path: Union[str, Path, None] = None,
177201
) -> str:
178-
"""Generate a self-contained HTML audit report.
202+
"""Generate a self-contained HTML report.
179203
180204
Parameters
181205
----------
@@ -219,14 +243,14 @@ def generate_html_report(
219243
ring_svg = f"""
220244
<svg class="ring-svg" width="112" height="112" viewBox="0 0 112 112">
221245
<circle cx="{cx}" cy="{cy}" r="{r}" fill="none"
222-
stroke="#b91c1c" stroke-width="{sw}"/>
246+
stroke="#fecaca" stroke-width="{sw}"/>
223247
<circle cx="{cx}" cy="{cy}" r="{r}" fill="none"
224-
stroke="#238636" stroke-width="{sw}"
248+
stroke="#16a34a" stroke-width="{sw}"
225249
stroke-dasharray="{green_len:.2f} {gap_len:.2f}"
226250
transform="rotate(-90 {cx} {cy})"/>
227-
<text x="{cx}" y="{cy - 6}" text-anchor="middle" fill="#f0f6fc"
251+
<text x="{cx}" y="{cy - 6}" text-anchor="middle" fill="#1c1917"
228252
font-size="15" font-weight="800">{cov_exact:.1f}%</text>
229-
<text x="{cx}" y="{cy + 12}" text-anchor="middle" fill="#8b949e"
253+
<text x="{cx}" y="{cy + 12}" text-anchor="middle" fill="#6b7280"
230254
font-size="9" letter-spacing="1">surrogate</text>
231255
</svg>"""
232256

@@ -236,17 +260,20 @@ def generate_html_report(
236260
<head>
237261
<meta charset="utf-8">
238262
<meta name="viewport" content="width=device-width, initial-scale=1">
239-
<title>TRACER - Audit Report</title>
263+
<title>Tracer routing report</title>
264+
<link rel="preconnect" href="https://fonts.googleapis.com">
265+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Manrope:wght@500;700;800&display=swap" rel="stylesheet">
240266
<style>{_CSS}</style>
241267
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
242268
</head>
243269
<body>
244270
<div class="page">
245271
246272
<div class="top-bar">
273+
<span class="dots"><i style="background:#0ea5e9"></i><i style="background:#f97316"></i><i style="background:#dc2626"></i></span>
247274
<span class="logo">TRACER</span>
248-
<span style="color:#30363d">|</span>
249-
<span style="font-size:.875rem;color:#8b949e">Audit Report</span>
275+
<span class="bar-sep">|</span>
276+
<span class="crumb">Routing report</span>
250277
</div>
251278
<h1>Routing Policy</h1>
252279
<div class="subtitle">

‎src/tracer/analysis/sankey.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def generate_sankey_div(
221221
artifact_dir: Union[str, Path],
222222
top_k: int = 15,
223223
) -> str:
224-
"""Return a self-contained HTML <div> with the Sankey chart embedded inline (dark theme).
224+
"""Return a self-contained HTML <div> with the Sankey chart embedded inline (light theme, matches the report).
225225
226226
Suitable for injecting directly into the HTML report page.
227227
Requires plotly to be installed (pip install tracer-llm[viz]).
@@ -242,7 +242,7 @@ def generate_sankey_div(
242242
manifest = _json.loads(manifest_path.read_text())
243243
qr = _json.loads(qr_path.read_text())
244244

245-
fig = _build_sankey_figure(manifest, qr, top_k, dark=True)
245+
fig = _build_sankey_figure(manifest, qr, top_k, dark=False)
246246
if fig is None:
247247
return ""
248248

‎src/tracer/cli/main.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,17 @@ def _cmd_fit(args):
2525
c = _C()
2626
header("fit", f"traces={args.traces} target-TA={args.target:.0%}")
2727

28-
config = FitConfig(target_teacher_agreement=args.target)
29-
30-
with step("Fitting routing policy (model zoo + calibration)"):
28+
# Tree surrogates (decision tree, random forest, extra-trees, gradient
29+
# boosting) are heavy and off by default; --trees opts them back in, and
30+
# --skip drops named candidates explicitly.
31+
TREES = ("dt", "rf", "et", "gbt")
32+
skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()}
33+
if not getattr(args, "trees", False):
34+
skip |= set(TREES)
35+
config = FitConfig(target_teacher_agreement=args.target,
36+
skip_candidates=tuple(sorted(skip)))
37+
38+
with step("Fitting routing policy (linear + MLP zoo + calibration)"):
3139
result = fit(args.traces, artifact_dir=args.artifact_dir, config=config)
3240

3341
_print_fit_result(result)
@@ -532,6 +540,13 @@ def main():
532540
help="Output artifact directory (default: .tracer)")
533541
p_fit.add_argument("--target", type=float, default=0.90,
534542
help="Target teacher agreement, e.g. 0.90 (default)")
543+
p_fit.add_argument("--trees", action="store_true",
544+
help="Add the tree-based surrogates (decision tree, random "
545+
"forest, extra-trees, gradient boosting). Off by default: "
546+
"they are slower and the linear/MLP heads are usually enough.")
547+
p_fit.add_argument("--skip", default="",
548+
help="Comma-separated surrogate models to skip from the zoo "
549+
"(e.g. dt,rf,et,gbt,mlp_1h).")
535550

536551
p_report = sub.add_parser("report", help="Show artifact manifest as JSON")
537552
p_report.add_argument("artifact_dir", nargs="?", default=".tracer")

‎src/tracer/traces/loader.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
from tracer.types import TraceDataset, TraceRecord
1717

18+
# Accepted key aliases (kept in sync with tracer.scanner). Canonical: input, teacher.
19+
_INPUT_KEYS = ("input", "query", "text", "prompt", "question")
20+
_LABEL_KEYS = ("teacher", "teacher_output", "label", "intent", "output", "answer")
21+
1822

1923
def load_traces(path: Union[str, Path]) -> TraceDataset:
2024
"""Load a JSONL trace file into a TraceDataset."""
@@ -31,12 +35,18 @@ def load_traces(path: Union[str, Path]) -> TraceDataset:
3135
raise ValueError(
3236
f"Malformed JSON in {path} at line {len(records) + 1}: {exc}"
3337
) from exc
34-
if "input" not in row or "teacher" not in row:
38+
# Accept common key aliases so middleware dumps load without
39+
# reshaping (mirrors the scan loader). Canonical keys are
40+
# 'input' and 'teacher'.
41+
input_key = next((k for k in _INPUT_KEYS if k in row), None)
42+
label_key = next((k for k in _LABEL_KEYS if k in row), None)
43+
if input_key is None or label_key is None:
3544
raise ValueError(
36-
f"Trace at line {len(records) + 1} is missing required fields "
37-
f"'input' and/or 'teacher'. Got keys: {list(row.keys())}"
45+
f"Trace at line {len(records) + 1} is missing an input and/or "
46+
f"teacher field. Accepted input keys: {_INPUT_KEYS}; "
47+
f"teacher keys: {_LABEL_KEYS}. Got keys: {list(row.keys())}"
3848
)
39-
teacher_val = row["teacher"]
49+
teacher_val = row[label_key]
4050
if teacher_val is None or (isinstance(teacher_val, float) and math.isnan(teacher_val)):
4151
raise ValueError(
4252
f"Trace at line {len(records) + 1} has a null/NaN 'teacher' field. "
@@ -46,7 +56,7 @@ def load_traces(path: Union[str, Path]) -> TraceDataset:
4656
if not isinstance(teacher_val, str):
4757
teacher_val = str(teacher_val)
4858
records.append(TraceRecord(
49-
input_text=str(row["input"]),
59+
input_text=str(row[input_key]),
5060
teacher_label=teacher_val,
5161
trace_id=str(row["id"]) if row.get("id") is not None else None,
5262
ground_truth=str(row["ground_truth"]) if row.get("ground_truth") is not None else None,

‎tests/test_fit.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_load_traces_missing_fields_raises():
9595
path = Path(tmp) / "missing.jsonl"
9696
with path.open("w") as f:
9797
f.write(json.dumps({"input": "hello"}) + "\n") # missing 'teacher'
98-
with pytest.raises(ValueError, match="missing required fields"):
98+
with pytest.raises(ValueError, match="missing an input and/or teacher"):
9999
load_traces(path)
100100

101101

0 commit comments

Comments
 (0)