HF — datasets + trace pipeline
reference· tech-lead · updated 2026-06-27 · source
SPIKE artifact for effort #943 / sub #947. Design only — no shippable code. This doc answers: where does the training DATA come from, and how does it reach Hugging Face in the right format, for the two training targets:
- Target A — the builder model (the dev-workflow corpus: effort→PR trajectories).
- Target B — per-tenant models (each PyME client's own corpus / knowledge).
Siblings: jobs & cost · isolation · serving.
0. The hard partition (read first)
Two corpora, never mixed — not with each other, not across tenants. This is the load-bearing constraint of the whole design:
- The dev-workflow corpus (Target A) is internal: our efforts, issues, PRs, diffs. It is the
"workflow" side of
workflow-vs-product.md. - The tenant corpora (Target B) are product data: each client's knowledge/content, one private dataset per tenant.
- ADR-006 states the rule verbatim: "Tenant/client data never mixes with the dev-workflow corpus (two datasets; connectors are product)." (ADR-006)
- The Plan Engine doc repeats it: the dev-workflow graph "must never mix with tenant/client data" (plan-engine.md §1, §7).
On HF this maps to: separate HF Dataset repos, ideally separate namespaces/orgs, separate tokens. Detailed isolation model (org vs namespace vs resource-group, token scoping) lives in isolation; this doc owns the data shapes and the pipeline.
1. Target A — the builder-model corpus
1.1 The workflow IS the dataset
ADR-006 ("the effort schema is the dataset schema") makes every effort a builder-agent trajectory: goal → plan → patch → verify → outcome. The unit of work and the unit of training are the same record (ADR-006, effort-model.md).
The parent-issue template maps 1:1 to dataset columns:
| Issue section | Dataset column | Meaning |
|---|---|---|
## Goal | problem_statement | what outcome, in 1–2 lines |
## Scope | plan | the sub-issue DAG (parallel | sequential) |
## For agents | retrieval_context | exact file paths / entry points |
| per-sub-issue diff | patch / patches[] | the code change (snapshot pre-squash) |
## Verification + outcome | verification / outcome_label | reward signal |
1.2 Where the trace lives — Postgres + a JSONL exporter (already built)
- Store: the Plan Engine Postgres graph (
planschema).plan_versionis append-only trajectory history;outcome_label(positive | negative | gold) carries the reward signal (plan-engine.md §2). - Exporter (exists today):
scripts/trace/export-traces.sh— "merged effort PRs → JSONL builder-trajectory records (ADR-006 Phase 5)". It reads merged effort PRs viagh, snapshots per-sub-issue diffs pre-squash (squash destroys the per-sub pairs), scrubs secrets from every diff/body, and emits one JSONL object per effort with exactly the columns above (problem_statement,plan,retrieval_context,repo_state,patches[],verification,outcome_label,gold,policy). Output is gitignored — only the tool is committed, never trace data. (Lineage: trace-export work tracked under issues #535 / #881; the live script isscripts/trace/export-traces.sh.)
So the front half of Target A is done: Postgres trace → export-traces.sh → JSONL. The HF spike
only needs to add the last hop: JSONL → HF Dataset (private).
1.3 JSONL → HF Dataset (private)
The JSONL is uploaded as a private HF Dataset under the org namespace, e.g.
tuempresadigital/builder-traces (private). Two equivalent paths:
- CLI:
hf upload tuempresadigital/builder-traces ./effort-traces.jsonl --repo-type dataset --private(private on first create; the CLI'shf repos create … --private+hf uploadalso works). - Python:
datasets.Dataset.from_json(...).push_to_hub("tuempresadigital/builder-traces", private=True)—private=Trueonly takes effect on first creation (datasets upload docs).
Training jobs read it back with load_dataset("tuempresadigital/builder-traces", token=True) (token
from the job's HF_TOKEN secret) — private datasets require the token at load time
(datasets upload docs).
1.4 Which TRL format the trace maps to
The trace is rich enough to feed two TRL methods, derived from the same JSONL:
- SFT (primary) — the natural fit. Build a conversational record per effort:
messages = [{role: system, ...}, {role: user, <problem_statement + retrieval_context + plan>}, {role: assistant, <patch>}]. TRL'sSFTTraineraccepts the conversationalmessagestype (or text / prompt-completion) (TRL dataset_formats). - DPO (later, from
outcome_label) — efforts that were reverted/closed-unmerged arenegative; the clean-merged version ispositive. Aprompt / chosen / rejectedtriple is derivable where both a good and a bad patch exist for the same problem. DPO requires exactlyprompt,chosen,rejectedcolumns (TRL dataset_formats). This is a follow-up; SFT lands first.
The shaping (JSONL → messages / prompt,chosen,rejected) is a dataset.map(...) step done in the
export pipeline (§4), before push, so the HF Dataset is already TRL-ready.
2. Target B — per-tenant data
- One private HF Dataset per tenant, never a shared one. Source = the tenant's own knowledge
corpus / content (the MemPalace-style palace
tuempresadigital/knowledgethat retrieval reads is the dev/knowledge corpus; tenant product data is separate per-client). Suggested naming:tuempresadigital/tenant-<tenantId>-corpus(private), or a dedicated per-tenant namespace — decided in isolation. - The
x-tedos-tenantseam already exists. The model contract carries a per-tenant header (TENANT_HEADER = "x-tedos-tenant"), sent only when atenantIdis supplied — the same id keys the per-tenant dataset and any per-tenant model (CONTRACT.md). - HARD RULE (no exceptions): tenant data NEVER mixes with the dev corpus or across tenants
(workflow-vs-product.md;
ADR-006 "two datasets; connectors are product"). On HF this means: distinct dataset repos, distinct
upload jobs, tenant-scoped tokens, and no cross-tenant
load_datasetin any single job. - Format: same TRL shapes as Target A — most tenant fine-tunes are SFT over the tenant's
Q&A / content (
messagesor prompt-completion). Per-tenant embeddings/RAG (a reranker or bi-encoder over the tenant corpus) is a separate, cheaper track viatrain-sentence-transformers— mentioned here only as an option; its dataset shapes (pairs / triplets / labeled pairs) and losses live in that skill, out of scope for this spike.
3. TRL dataset formats + validation
3.1 Required column shapes per method
| Method | Dataset type | Required columns | Notes |
|---|---|---|---|
| SFT | conversational / language-modeling / prompt-completion | messages or text or prompt+completion | most flexible; the default builder + tenant path |
| DPO | preference (explicit prompt) | prompt, chosen, rejected | strictest — exact names; ~90% of raw datasets need a map |
| GRPO | prompt-only | prompt | model generates; reward computed online by a reward fn (no stored completions) |
| Reward | preference | chosen, rejected | for an RLHF reward model |
Source: TRL dataset_formats
and huggingface-llm-trainer skill (references/training_methods.md).
For the builder model: SFT now, DPO when negative-outcome pairs accumulate. GRPO is plausible later (verifiable rewards = "did the patch make the build/tests pass") but needs an online reward function, not just a dataset — defer.
3.2 Validation BEFORE GPU spend (mandatory gate)
The huggingface-llm-trainer skill is emphatic: validate the dataset format on CPU before any GPU
job. ~50%+ of training failures are format mismatches; DPO especially (exact column names). CPU
validation costs ~$0.01 and <1 min; a failed GPU job wastes $1–10 and 30–60 min.
- Tool:
dataset_inspector.py(in the skillscripts/, also athttps://huggingface.co/datasets/mcp-tools/skills/raw/main/dataset_inspector.py). - Run:
hf jobs uv run --flavor cpu-basic … dataset_inspector.py --dataset tuempresadigital/builder-traces --split train. - Output markers:
✓ READY(use directly) /✗ NEEDS MAPPING(emits copy-pastemapcode) /✗ INCOMPATIBLE. - Gate rule for both targets: export → push private HF Dataset →
dataset_inspector→ only on✓ READYdoes a GPU training job launch. This is the contract between this doc and jobs & cost.
4. Where the pipeline lives — the Python satellites
Two external Python repos already own the data/training side and stay out of the monorepo (they
would poison the turbo graph: weights + MLX + Python). They couple to tedos only through the wire
contract (/v1 + x-tedos-tenant) (CONTRACT.md):
| Satellite | Today's job | Fits export→HF? |
|---|---|---|
tuempresadigital/chat-datasets | dataset processing + MLX (149 MB weights) | Yes — owns dataset processing. Natural home for JSONL→TRL-shape map + push to HF Dataset + dataset_inspector gate. |
tuempresadigital/tedos-builder | corpus + tb sync (pushes the knowledge corpus retrieval reads), Python dev tool | Owns corpus sync/triggering, not dataset shaping. |
Recommendation — split by concern
chat-datasetsOWNS the export→HF-Dataset step (shaping + validation + push) for both targets. It is already the "dataset processing" satellite; adding "shape JSONL →messages/prompt,chosen,rejected, push private HF Dataset, rundataset_inspector" is squarely its job. For Target A it consumes the JSONL emitted byscripts/trace/export-traces.sh; for Target B it consumes each tenant corpus, one isolated run per tenant.tedos-builderTRIGGERS it. It already owns scheduled/post-merge sync (tb sync"must run on a schedule or post-merge"), and the repo runs no GitHub Actions CI (billing-blocked — gates are Vercel build + local), so the canonical home of any schedule is the satellite's own repo (CONTRACT.md §Knowledge sync).tedos-builderinvokes thechat-datasetsexport entrypoint.
Cadence
| Target | Trigger | Why |
|---|---|---|
| A — builder corpus | post-merge (effort PR lands → export-traces.sh for that PR → refresh the HF Dataset), with a hf jobs scheduled nightly catch-up sweep | matches kit-effort-close (snapshot pre-squash is already part of close); the merge IS the new training example, so append it then |
| B — tenant corpus | scheduled per tenant (hf jobs scheduled run "<cron>" …) + on-demand re-export when a tenant's content changes materially | tenant content changes continuously; a per-tenant cron keeps each dataset fresh without coupling tenants |
hf jobs scheduled uv run <SCHEDULE> <SCRIPT> (cron syntax, e.g. "0 3 * * *") runs the export/push
as a managed scheduled job on HF infra — no CI runner needed (verified live: hf jobs scheduled --help). Pushing/training requires HF_TOKEN passed as a job secret (secrets={"HF_TOKEN": "$HF_TOKEN"}); the environment is ephemeral, so the dataset must be pushed to the Hub or it is
lost (llm-trainer skill).
5. End-to-end data-flow diagrams
Target A — builder model (dev-workflow corpus)
effort/<N> → sub-issues merge → effort PR merged to develop
│ │ (kit-effort-close: snapshot sub-diffs PRE-squash)
▼ ▼
Plan Engine Postgres ───────────► scripts/trace/export-traces.sh
(plan_version, outcome_label) │ • one JSONL record / effort
│ • secrets scrubbed, diffs pre-squash
▼
effort-traces.jsonl (gitignored, local)
│
post-merge + nightly `hf jobs scheduled` (tedos-builder triggers → chat-datasets runs)
▼
chat-datasets: map → TRL shape (SFT `messages`; DPO `prompt/chosen/rejected`)
▼
hf upload → HF Dataset (PRIVATE) tuempresadigital/builder-traces
▼
dataset_inspector.py (cpu-basic gate) → ✓ READY ?
▼ yes
hf jobs uv run (TRL SFTTrainer, GPU) ──► model on Hub tuempresadigital/builder-model
Target B — per-tenant model
tenant <tenantId> corpus / knowledge (product data; x-tedos-tenant seam)
│ ONE isolated run per tenant — never combined
scheduled per tenant: `hf jobs scheduled run "<cron>" …` (tedos-builder triggers → chat-datasets runs)
▼
chat-datasets: map → TRL shape (usually SFT `messages` / prompt-completion)
▼
hf upload → HF Dataset (PRIVATE, tenant-scoped) tuempresadigital/tenant-<tenantId>-corpus
▼
dataset_inspector.py (cpu-basic gate) → ✓ READY ?
▼ yes
hf jobs uv run (TRL SFT, GPU, tenant-scoped token) ──► model on Hub tuempresadigital/tenant-<tenantId>-model
│
└─ (optional, separate track) train-sentence-transformers → per-tenant reranker/embeddings for RAG
Both flows end at a model on the Hub; how that model is served back through /v1 +
x-tedos-tenant is serving; GPU flavor/cost/scheduling is
jobs & cost; the repo/namespace/token boundary that keeps A≠B and tenant≠tenant
is isolation.
Implications for the ADR (#948)
- Reuse, don't rebuild. The front half of the builder pipeline already exists
(
scripts/trace/export-traces.sh, Postgresplan_version/outcome_label). The ADR only needs to adopt the last hop: JSONL → private HF Dataset, plus the TRL-shapemap. Cheap delta. - Two datasets, two HF homes, by construction. The ADR must mandate separate private HF Dataset repos (and tenant-scoped tokens) for dev-corpus vs each tenant — the existing hard rule (ADR-006 / workflow-vs-product) becomes an HF-repo-layout rule. No single job ever loads more than one tenant.
- Validation is a required gate, not optional.
dataset_inspectoroncpu-basicmust pass (✓ READY) before any GPU job — make it a step in the pipeline contract. - Format: SFT first, DPO later. Ship SFT (
messages) for both targets; DPO (prompt/chosen/rejected) is a follow-up onceoutcome_labelnegatives accumulate. GRPO/reward are out of scope for the MVP. - Ownership:
chat-datasetsowns export→HF-Dataset;tedos-buildertriggers it. Cadence: builder = post-merge + nightlyhf jobs scheduledcatch-up; tenant = per-tenant cron + on-demand. No new CI — HF scheduled jobs replace the (absent, billing-blocked) GitHub Actions runner. - Secrets + monolingual corpus discipline carry over. export-traces already scrubs secrets; keep that on the tenant path too. Artifacts/corpus stay English (clean monolingual builder corpus) per ADR-006.
Sources
- Repo: ADR-006 ·
effort-model.md ·
plan-engine.md ·
workflow-vs-product.md ·
model-client CONTRACT.md ·
scripts/trace/export-traces.sh·huggingface-llm-trainer+train-sentence-transformersskills. - HF: Share/upload a dataset (private, push_to_hub, load_dataset token) · TRL dataset formats · TRL docs · HF Jobs guide.
- Live CLI (read-only):
hf datasets --help,hf jobs scheduled --help(cronhf jobs scheduled uv run <SCHEDULE> <SCRIPT>).