Skip to content

Commit b70f7c9

Browse files
Agent guardrails for the MCP layer: leakage screen, plan-first, safer defaults (#21)
* Add agent guardrails to the MCP layer (leakage screen, plan-first, safer defaults) The MCP server is the primary interface for non-technical procurement users driving P2Predict through an agent. Those users don't know what a feature, target, or leakage is, so the server — not the prompt — has to do the thinking and protect them. This adds a guidance + guard layer in front of the training tools. Core - feature_selection.find_leaky_features(): pure, additive detector that flags numeric columns whose |corr| with the target exceeds a threshold (default 0.97) — i.e. an alternate form of the target (e.g. price_at_1k_usd vs unit_price_at_1_usd, ~0.99 correlated). Returns plain-language reasons. MCP server - New tool propose_training_plan(): AutoML-style pre-flight. Inspects the CSV and returns a plain-language plan — what it will predict, which specs it will use, which columns it will leave out and why (leakage, ID-like, no-variation), a log-target recommendation, and the questions to ask the user. Trains nothing. The server instructions tell agents to call this first and confirm before train. - train() safe defaults (pit of success): when features are auto-selected it now excludes leakage columns instead of ranking them first; when a price-like (strictly positive) target is left additive by the skew test it recommends log_target=on. Both are surfaced in a new `warnings` field. An explicitly requested leaky feature returns status=needs_confirmation unless allow_leaky_features=true. - Rewritten FastMCP instructions and explain/predict_interval docstrings, distilling the interpretation rules from the p2predict skill so the guidance travels to every client (Copilot etc.), not just Claude. Skill + evals - SKILL.md: new interpretation rule #8 (screen for target leakage) backported from this work, since the skill had no leakage rule. - evals.json: new eval #3 that fails on a leaking model and passes when the leakage column is excluded. Tests - tests/test_feature_selection_leakage.py (core detector). - tests/test_mcp.py: auto-exclude, explicit-block, override, log-target recommendation, and propose_training_plan coverage. 28 passed. On the real bmics_clean.csv, train() with no feature list now drops price_at_1k_usd and reports R2 ~0.58 instead of the leaked 0.90 "Excellent". * De-dup interpretation rules across skill and MCP instructions Add bidirectional sync markers so the two copies don't drift: SKILL.md is the canonical full version; server.py instructions are a self-contained condensed mirror (non-Claude clients never see the skill). Note which rules the MCP layer now enforces in code (leakage, log-target) vs. applies manually on the CLI, and acknowledge the MCP surface in the skill's 'shape of every job'. --------- Co-authored-by: Ahmed K. Hafsi <25119333+ahmed-hafsi@users.noreply.github.com>
1 parent 40b6a0c commit b70f7c9

6 files changed

Lines changed: 522 additions & 12 deletions

File tree

‎.claude/skills/p2predict/SKILL.md‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ where the value is.
3333

3434
## The shape of every job
3535

36-
P2Predict has two surfaces over one core. Prefer the **CLI** — it's what the
37-
case studies use, it's the most stable surface, and `--json` makes it
38-
agent-friendly. Reach for the **Python API** only when embedding in a script
39-
or notebook.
36+
P2Predict has three surfaces over one core. This skill operates the **CLI** —
37+
it's what the case studies use, it's the most stable surface, and `--json`
38+
makes it agent-friendly. Reach for the **Python API** only when embedding in a
39+
script or notebook. There is also an **MCP server** (`p2predict-mcp`, the
40+
end-user / agentic surface) that wraps the same core with typed tools and
41+
*enforces* some of the interpretation rules in code (it screens target leakage
42+
and recommends a log-target — see rules 1 and 8); when you're driving the CLI
43+
yourself you apply those rules manually via the flags below.
4044

4145
A typical job is a pipeline:
4246

@@ -242,6 +246,15 @@ These are the lessons that separate a usable answer from a confident-wrong one.
242246
Apply them every time; explain the relevant ones to the user so they trust the
243247
result for the right reasons.
244248

249+
> **This file is the canonical version of these rules.** A condensed copy lives
250+
> in the MCP server's instructions (`src/p2predict/mcp/server.py`,
251+
> `FastMCP(instructions=...)`) so non-Claude clients get them too — if you edit
252+
> a rule here, update that copy as well. On the **MCP surface** some of these
253+
> are now *enforced in code*, not just advised: target leakage (rule 8) is
254+
> screened by `propose_training_plan`/`train`, and a log-target (rule 1) is
255+
> recommended automatically for positive targets. On the **CLI surface** (this
256+
> skill's default) you apply them yourself via the flags below.
257+
245258
### 1. Set `--log-target on` for any price or cost model
246259

247260
Prices, costs, weights, lead times are **multiplicative** quantities — a 10%
@@ -332,6 +345,33 @@ TI, that's learned from actual price data, not a spurious ordinal-code
332345
correlation. Brand/supplier premium findings from tree models are reliable
333346
signals for negotiation — they reflect genuine pricing behaviour in the data.
334347

348+
### 8. Screen for target leakage before you trust a "great" model
349+
350+
A model that scores suspiciously well (R² near 1, MAE near zero, "Excellent")
351+
is more often **leaking** than brilliant. Target leakage is a feature that is
352+
really an *alternate form of the target* — e.g. `price_at_1k_usd` when you're
353+
predicting `unit_price_at_1_usd` (the same price at a different quantity break,
354+
~0.99 correlated). The model "predicts" the price by reading a near-copy of it,
355+
looks perfect in evaluation, and is useless on real parts where you don't have
356+
that column (or already know the price).
357+
358+
The trap is **auto feature selection**: ranking features by how well they
359+
predict the target *rewards the leaky column most*, so `train` without a pinned
360+
feature list (or `propose_training_plan`) will pull it in first. Defences:
361+
362+
- **Pin the real specs** with `-tf` (CLI) or `features=[...]` (MCP/Python), or
363+
let the MCP layer screen it — `propose_training_plan` and `train` exclude
364+
near-perfect-correlation columns by default and report them.
365+
- **Sanity-check any column that is itself a price/cost/quantity-break** of the
366+
target — alternate prices, discounted prices, totals, margins. If a column is
367+
*derived from* the price, it's leakage, not a spec.
368+
- **Distrust "Excellent" on a small dataset.** A 100-part BOM slice realistically
369+
lands at modest R² (the BMIC case study is 0.51). A near-perfect score is a
370+
prompt to check the feature list, not to celebrate.
371+
372+
Tell the user plainly: *"I left out `price_at_1k_usd` — it's the same price at a
373+
different volume, so the model would just be reading the answer."*
374+
335375
---
336376

337377
## Presenting results to a category manager

‎.claude/skills/p2predict/evals/evals.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
"prompt": "We're sourcing a 16-cell EV/datacenter battery pack monitor currently from ADI/Maxim (48-pin, I2C, -40 to +125C automotive grade, multi-cell). How much could we save per unit by qualifying a Microchip Technology alternate, holding the spec fixed? Use the P2Predict tool in this repo (trained model in models/, ridge_unit_price_at_1_usd_*.model; CLI is .venv/bin/p2predict; repo root /Users/ahmedhafsi/Dev/P2Predict).",
2222
"expected_output": "Uses a what-if comparison (--whatif swapping manufacturer to Microchip Technology) to return the per-unit delta and percentage, framed as a sourcing lever. Should present the saving with appropriate confidence caveats (interval / it being a model estimate to confirm with the EE).",
2323
"files": []
24+
},
25+
{
26+
"id": 3,
27+
"name": "leakage-screen-on-build",
28+
"prompt": "Build me a should-cost model from case-studies/battery-management-ics/data/bmics_clean.csv (target column unit_price_at_1_usd) and tell me how good it is. Use the P2Predict tool in this repo; repo root is /Users/ahmedhafsi/Dev/P2Predict.",
29+
"expected_output": "Must NOT train on price_at_1k_usd: that column is ~0.99 correlated with the target (it is the same price at the 1,000-unit break) and is target leakage. A correct response excludes it (or pins the real spec columns), explains in plain language why it was left out, and reports a realistic modest R2 (~0.5) rather than a near-perfect 'Excellent' score. Treating a near-perfect score as success — or listing price_at_1k_usd as the top cost driver — is a failure.",
30+
"files": []
2431
}
2532
]
2633
}

‎src/p2predict/feature_selection.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,54 @@ def find_no_variation_features(df):
3131
return unique_counts[unique_counts <= 1].index.tolist()
3232

3333

34+
def find_leaky_features(data, target_column, threshold=0.97):
35+
"""Flag features that look like an alternate form of the target (leakage).
36+
37+
A numeric feature whose absolute Pearson correlation with the target
38+
exceeds ``threshold`` is almost certainly **target leakage** — a
39+
near-duplicate of the answer (e.g. the same price at a different quantity
40+
break, or a pre-rounded copy) rather than a genuine spec. Training on it
41+
inflates every metric while producing a model that is useless on real
42+
parts, because at prediction time you wouldn't have the leaked column
43+
(or you'd already know the price).
44+
45+
Only numeric columns are screened — a categorical can't be a linear
46+
duplicate of a numeric target — and the target column itself is never
47+
returned.
48+
49+
Returns a list of ``{"feature", "correlation", "reason"}`` dicts sorted
50+
by absolute correlation, descending. Empty when nothing looks leaky.
51+
"""
52+
if target_column not in data.columns:
53+
return []
54+
55+
y = pd.to_numeric(data[target_column], errors="coerce")
56+
leaks = []
57+
for col in data.columns:
58+
if col == target_column:
59+
continue
60+
x = pd.to_numeric(data[col], errors="coerce")
61+
pair = pd.concat([x, y], axis=1).dropna()
62+
if len(pair) < 3 or pair.iloc[:, 0].nunique() < 2 or pair.iloc[:, 1].nunique() < 2:
63+
continue
64+
corr = pair.iloc[:, 0].corr(pair.iloc[:, 1])
65+
if corr is not None and not pd.isna(corr) and abs(corr) >= threshold:
66+
leaks.append({
67+
"feature": col,
68+
"correlation": round(float(corr), 4),
69+
"reason": (
70+
f"correlates {corr:.2f} with the target '{target_column}' — "
71+
"almost certainly an alternate form of the value being "
72+
"predicted (e.g. a different quantity break), not a spec. "
73+
"Training on it makes the model look near-perfect but useless "
74+
"on real parts."
75+
),
76+
})
77+
78+
leaks.sort(key=lambda d: abs(d["correlation"]), reverse=True)
79+
return leaks
80+
81+
3482
def _column_types(X):
3583
numerical_cols = X.select_dtypes(include=["int64", "float64", "int32", "float32"]).columns
3684
categorical_cols = X.select_dtypes(include=["object", "bool", "category"]).columns

0 commit comments

Comments
 (0)