-
-
GitHub issue — PII Exposure incident with entity details and downstream model reference
-
Full end-to-end demo run — baseline PII detection, schema drift trigger, and second run catching both incidents
-
GitHub issue — PII Exposure incident with entity details and downstream model reference
-
Slack notifications — PII exposure and schema drift incidents posted with GitHub issue links
-
DataHub UI — customers dataset showing PII tag and 4-column schema (post schema-change)
-
DataHub UI — customers dataset showing PII tag and 4-column schema (post schema-change)
Inspiration
ML models fail silently. A source table gets a column renamed, a new PII-tagged field gets added upstream, a data source quietly goes stale — and nothing breaks loudly. Model quality just slowly degrades until someone notices weeks later, usually the hard way. The information to catch this earlier already exists: DataHub's lineage graph knows exactly what feeds into every model. The problem is that a human has to remember to go look. We wanted an agent that looks automatically.
What it does
ML Incident Copilot walks a production ML model's real upstream lineage graph in DataHub — training data → features → model — using the DataHub MCP Server. At each node it checks for:
- Schema drift — columns added, removed, or renamed since the last check
- PII exposure — sensitive/PII-tagged data feeding a model without documented justification
- Stale data — sources that haven't refreshed within their SLA
- Deprecated sources — deprecated/archived datasets still live in a production lineage path
When it finds something, it doesn't just alert — it writes the incident directly back onto the affected entity in DataHub (a tag, a structured property, and a saved incident document), so the next engineer or agent who opens that table in DataHub inherits full context immediately. It also opens a GitHub issue and posts to Slack, so a human gets pulled in outside the catalog too.
How we built it
The core is a Python agent (agent/main.py) that connects to
mcp-server-datahub over stdio using the official MCP client library,
walks lineage via the get_lineage tool, pulls schema via
list_schema_fields, runs a set of pluggable risk checks
(agent/risk_checks.py) against each node, and hands off anything it
finds to a write-back layer (agent/writeback.py) that talks to
DataHub, GitHub, and Slack.
We seeded a realistic lineage graph ourselves: raw.customers
(PII-tagged) and raw.transactions feeding two real MLFeature
entities, which feed an MLModel entity (churn_predictor_v1) via
its mlFeatures property — deliberately using real DataHub entity
types and a real, graph-indexed lineage path rather than a shortcut.
Challenges we ran into
Almost everything here was discovered the hard way against a live DataHub instance rather than assumed from docs:
- Our first attempt at model→feature lineage used the
mlModelTrainingDataaspect — it emits successfully but isn't indexed by DataHub's lineage graph at all. We only found this by checking the UI and seeing an empty Lineage tab, then found the underlying GitHub issue confirming it. Switching to realMLFeatureentities with an indexedsourcesfield fixed it for good. - The MCP server's tool response shapes are not uniform.
searchwraps results insearchResults,get_lineagenests them insideupstreams/downstreams,list_schema_fieldsreturns a flatfieldsarray, andget_entitieswraps everything inresult. We had to inspect each one individually via the MCP Inspector against live data — an "empty" test call withtotal: 0looks identical to a working call with the wrong field name, so we learned to always verify with a populated result before trusting a shape. - Tags came back in a nested shape we didn't expect —
{"tags": [{"tag": {"urn": "urn:li:tag:PII"}}]}— with the tag name only recoverable by parsing the urn suffix, not a cleannamefield. - A subtle Python bug: caching a
set()to JSON viajson.dumps(..., default=str)silently corrupts it into a string instead of erroring, which made schema-drift detection appear to work at first but quietly never actually compare anything. - Windows-specific friction throughout —
python3vspython,setvsexport, Python 3.14 failing to buildpydantic-core(fixed by pinning to 3.11), and Docker Desktop needing more memory than its default to run the full DataHub quickstart stack.
Accomplishments that we're proud of
- A fully working, end-to-end agent against a real DataHub instance — not a mocked demo. Every tool call, every response shape, every write-back was verified against a live server, not assumed.
- Getting model→feature→dataset lineage genuinely indexed and visible in DataHub's own Lineage UI, after an earlier approach silently failed to show up there at all.
- Catching and fixing a real, non-obvious bug (the
set-to-JSON serialization issue) that would have made schema-drift detection quietly never work, without ever throwing an error to flag it. - A complete round trip: an actual schema change in DataHub triggers detection, a tag + structured property + saved document written back onto the entity, a GitHub issue, and a Slack message — all from one command.
- Documentation good enough that someone else (or a judge) could actually get this running, including exact steps for getting a DataHub token, a GitHub token, and a Slack webhook — the parts that are usually left as an exercise for the reader.
What we learned
Trust the live response over the docstring. Almost every real bug here came from an assumption about a tool's output shape that turned out to be subtly wrong — and every one of them was resolved fastest by adding a raw debug print and looking at the literal dictionary, rather than guessing again. We also learned that DataHub's various aspects aren't interchangeable for lineage purposes — which aspect you use to link entities determines whether the graph is actually queryable, not just whether the emit call succeeds.
What's next for ML Incident Copilot
The README includes a full "Path to Production" section, but in short: move the local JSON cache to shared storage so multiple runs don't clobber each other, move from manual/cron runs to DataHub's own Actions framework so this reacts to changes in real time instead of on a schedule, add incident state tracking (open/acknowledged/resolved) so it stops re-notifying on already-known issues, and paginate lineage walks for catalogs with real scale instead of a flat 30-result call.
Log in or sign up for Devpost to join the conversation.