Pingpin Context

Inspiration

I built Pingpin, a multi-agent system that auto-generates Etsy listings, and put it into real use on an actual shop. That's when I ran into a gap Pingpin never solved: once a listing goes live, nothing tracks whether its description has actually been kept current. New sellers especially live and die by their title/description keywords — visibility comes from testing and retesting them, and that's manageable for one listing. But once you're running ten, or refreshing everything for a promotion season, tracking what changed and when turns into a spreadsheet nobody wants to maintain.

That's a state-management problem underneath a content problem. And solving it properly meant an agent that could actually understand the data it was querying — not guess at column meanings from a table name. That's what pointed me toward DataHub.

What it does

Pingpin Context is an inspection agent that:

  • Connects to DataHub through the MCP Server and confirms the real schema of a live dbt project before querying anything.
  • Queries a DuckDB-backed SCD Type 2 table for listings whose descriptions have gone stale (haven't changed in N days).
  • Builds a layered report — separating facts (measured, non-negotiable) from inference (a rule-derived recommendation) — and hands it to a human via a LangGraph interrupt().
  • Once a human picks which SKUs to rewrite and supplies keywords/tone, it drives the request through Pingpin's real, already-running A1–A5 pipeline — not a shortcut, the same path a fresh listing takes.
  • Once approved, the new version writes back to the raw table, dbt snapshot versions it as a new SCD2 record, and datahub ingest reflects the change in DataHub's lineage graph — so the next inspection, or any other agent querying the same graph, inherits the updated history.

How I built it

  • dbt + DuckDB for the data layer: a dim_sku registry (persisted surrogate keys, not hashed — because sellers can rename SKUs without losing history), an incremental raw_description_updates model, and a description_history SCD Type 2 snapshot.
  • DataHub (local quickstart) ingests the dbt project's manifest.json/catalog.json to build a real, queryable catalog and lineage graph.
  • DataHub's MCP Server (mcp-server-datahub) is the agent's read path into that catalog — verified non-hallucinated by checking that get_entities returns this project's actual schema fields, not an invented example.
  • LangGraph powers both the inspection agent's own single-node graph (needed because interrupt() only works inside a compiled graph context) and Pingpin's existing 5-agent pipeline (compliance check → SEO extraction → draft → hard-gate + LLM audit → final approval), which the inspection agent drives directly rather than reimplementing.
  • Pydantic models keep the facts/inference/rewrite data contracts structured and typed end to end.

Challenges I ran into

  • A silent SKU-identity bug. The write-back path originally looked up SKUs by product name instead of by ID, so an approved rewrite for a known SKU (S001) was silently treated as "not found" and minted a brand-new, duplicate SKU. Caught by checking the actual database row, not by trusting a log that said "success."
  • A dbt model quietly wiping real data. The raw table was materialized as table instead of incremental — every dbt run rebuilt it from scratch, erasing whatever the agent had just written, including via an unrelated background process re-running dbt run periodically. Same lesson: verify the data, not the log output.
  • Getting MCP working from real Python code, not just a CLI. Verifying the connection worked from mcp-cli was one thing; wiring stdio_client + ClientSession + async interrupt() handling into a working script was a different, more fiddly problem — solved by building it up method by method and testing each layer before combining them.

Accomplishments that I'm proud of

Getting a fully verified, real end-to-end run: a genuinely stale SKU flagged → schema confirmed via MCP → human approval → routed through Pingpin's actual production pipeline, including a real hard-gate rejection and retry. From there it was written back, with dbt snapshot correctly closing out the old version and opening a new one, and DataHub's lineage graph reflecting the change. Every step backed by a real database query, not a screenshot I trusted on faith.

What I learned

That "the code ran without an error" and "the result is actually correct" are two different claims, and conflating them is how bugs like the SKU-identity mixup and the data-wiping materialized='table' issue slip through. Also: MCP's real value here wasn't in writing to DataHub — it was proving, before any query ran, that the agent's understanding of the schema was grounded in something real.

What's next for Pingpin Context

  • Move Pingpin's A4 hard-gate use-case check from exact-phrase matching to semantic matching, since it can currently reject listings that convey a valid use-case in different wording.
  • A persisted "original brief" store, so rewrites can draw on the seller's original creative intent rather than the prior description alone.
  • Extending the inspection signal beyond time-since-update — e.g. incorporating sales/conversion signals — to prioritize which stale listings actually matter most.

Built With

  • datahub
  • dbt
  • duckdb
  • langchain
  • langgraph
  • mcp
  • pydantic
  • python
Share this project:

Updates