neurojenml-api / datasets.py
sciedoc's picture
Sync from GitHub
d31e494 verified
Raw
History Blame Contribute Delete
38.3 kB
"""
NeuroJenML — Dataset builder, merger, validator, and HF export.
Architecture:
- Neon stores manifests (metadata, IDs, usage tracking)
- HF stores the actual JSONL files (single source of truth)
- Training pulls JSONL from HF by path, same file that's in the dataset viewer
- No JSONL stored inline in Neon — keeps the DB lean and avoids drift
Flow: build → push to HF → store manifest → (merge) → validate → train → mark used
"""
import os
import io
import json
import time
import asyncio
from datetime import datetime
import store
# ─── JSONL builders ──────────────────────────────────────────────
# Maximum training examples generated from a single paper.
# Each map edge produces 3 examples; without a cap, papers with many edges
# (e.g. cardiovascular / MI papers with 12+ edges → 36+ examples) numerically
# dominate the training set and cause domain drift.
_MAX_EXAMPLES_PER_PAPER = 15
def _systemic_examples(extractions: list) -> list:
examples = []
for ext in extractions:
fields = ext.get("fields", {})
cls = ext.get("classification", {})
tags = cls.get("systemic_tags", [])
hypothesis = fields.get("hypothesis", "")
results = fields.get("results", "")
maps = fields.get("maps", []) or []
# Track how many examples this single paper contributes so that papers
# with many edges (e.g. cardiovascular papers with 12+ edges) cannot
# dominate the training set.
paper_example_count = 0
for m in maps:
if not (m.get("from") and m.get("to")):
continue
if paper_example_count >= _MAX_EXAMPLES_PER_PAPER:
break
relation = m.get("relation", "relates_to")
# Build a rationale that is explicitly conditioned on THIS edge
# (from → relation → to), not a raw slice of the paper body.
# A short context snippet is appended for grounding; the edge
# description is always the lead sentence so the model learns the
# correct association rather than surface co-occurrence.
context_snippet = (results or hypothesis)[:200].strip()
rationale = (
f"{m['from']} drives '{relation}', leading to {m['to']}."
+ (f" Context: {context_snippet}" if context_snippet else "")
)
# Task 1: Mechanism inference — given correlation, predict mechanism
examples.append({
"task_type": "mechanism_inference",
"instruction": (
"Given an observed association between a peripheral/systemic factor and a "
"central (brain) outcome in Alzheimer's disease, state the most likely "
"bridging mechanism. Respond as JSON with keys 'mechanism', 'rationale'."
),
"input": f"Association: '{m['from']}' is linked to '{m['to']}'. What mechanism bridges them?",
"output": json.dumps({
"mechanism": relation,
"rationale": rationale,
}),
"systemic_tags": tags,
"paper_id": ext.get("paper_id"),
"provenance": ext.get("provenance", {}),
})
# Task 2: Forward prediction — given peripheral factor, predict central outcome
examples.append({
"task_type": "forward_prediction",
"instruction": (
"You are a systemic Alzheimer's reasoning model. Given a peripheral "
"(body-level) finding, predict the downstream brain pathology and the "
"biological mechanism that links them. Respond as JSON with keys "
"'mechanism', 'central', 'rationale'."
),
"input": f"Peripheral finding: {m['from']}.",
"output": json.dumps({
"mechanism": relation,
"central": m["to"],
"rationale": rationale,
}),
"systemic_tags": tags,
"paper_id": ext.get("paper_id"),
"provenance": ext.get("provenance", {}),
})
# Task 3: Reverse prediction — given central outcome, predict peripheral cause
examples.append({
"task_type": "reverse_prediction",
"instruction": (
"Given a central brain pathology in Alzheimer's disease, identify "
"the peripheral (body-level) factors that may contribute to it. "
"Respond as JSON with keys 'peripheral', 'mechanism', 'rationale'."
),
"input": f"Central pathology: {m['to']}.",
"output": json.dumps({
"peripheral": m["from"],
"mechanism": relation,
"rationale": rationale,
}),
"systemic_tags": tags,
"paper_id": ext.get("paper_id"),
"provenance": ext.get("provenance", {}),
})
paper_example_count += 3 # 3 tasks per edge
return examples
def _reasoning_examples(extractions: list) -> list:
"""Generate chain-of-thought (CoT) reasoning training examples.
Three reasoning task types that force the model to think rather than retrieve:
reasoning_chain
A 5-step structured CoT trace wrapped in <think>...</think> before the
final answer. Each step is labelled so the model learns to scaffold
its own reasoning process.
counterfactual_reasoning
"If X were absent or successfully treated, what would happen to Y?"
Forces causal-direction reasoning rather than rote association recall.
multi_hop_chain
When two edges from the same paper share a middle node (A->B, B->C),
generate a chain question: "given only A, trace through to C." This
teaches the model to compose two-step inference rather than looking up
a direct link.
"""
examples = []
for ext in extractions:
fields = ext.get("fields", {})
cls = ext.get("classification", {})
tags = cls.get("systemic_tags", [])
hypothesis = fields.get("hypothesis", "")
results = fields.get("results", "")
maps = fields.get("maps", []) or []
paper_id = ext.get("paper_id")
provenance = ext.get("provenance", {})
paper_reasoning_count = 0
# Index edges by their 'from' node so we can find chains A->B, B->C.
edge_index: dict = {}
for m in maps:
if m.get("from") and m.get("to"):
edge_index.setdefault(m["from"], []).append(
(m.get("relation", "relates_to"), m["to"])
)
for m in maps:
if not (m.get("from") and m.get("to")):
continue
if paper_reasoning_count >= _MAX_EXAMPLES_PER_PAPER:
break
src = m["from"]
tgt = m["to"]
relation = m.get("relation", "relates_to")
context_snippet = (results or hypothesis)[:200].strip()
# ── Task A: reasoning_chain ──────────────────────────────────────
# A labelled 5-step CoT inside <think> blocks, followed by the final
# JSON answer. The model learns to narrate each inference step.
think_block = (
"<think>\n"
f"Step 1 - Identify domain: '{src}' is a peripheral/systemic factor; "
f"'{tgt}' is a central brain outcome in Alzheimer's disease.\n"
f"Step 2 - Recall known link: prior evidence associates {src} with {tgt} "
f"through the '{relation}' pathway.\n"
f"Step 3 - Biological mechanism: {src} drives '{relation}', which in the "
f"CNS context results in {tgt}. "
+ (f"Supporting context: {context_snippet}." if context_snippet else "") + "\n"
f"Step 4 - Confidence assessment: the relationship is supported by "
f"experimental evidence from the source paper.\n"
f"Step 5 - Final answer: the mechanism is '{relation}'; the downstream "
f"central pathology is '{tgt}'.\n"
"</think>"
)
examples.append({
"task_type": "reasoning_chain",
"instruction": (
"Think step-by-step before answering. "
"Given a peripheral/systemic factor, reason through the biological "
"pathway to identify the central (brain) outcome in Alzheimer's disease. "
"First show your reasoning inside <think>...</think> tags, then respond "
"with JSON: {\"mechanism\": ..., \"central\": ..., \"confidence\": ...}."
),
"input": f"Peripheral factor: {src}.",
"output": (
think_block + "\n"
+ json.dumps({
"mechanism": relation,
"central": tgt,
"confidence": "supported by experimental evidence",
})
),
"systemic_tags": tags,
"paper_id": paper_id,
"provenance": provenance,
})
paper_reasoning_count += 1
# ── Task B: counterfactual_reasoning ─────────────────────────────
# Force causal-direction reasoning: the model must reason about what
# would *not* happen if the upstream factor were removed or treated.
cf_think = (
"<think>\n"
f"Counterfactual premise: suppose '{src}' is absent or successfully treated.\n"
f"Step 1 - Causal chain: '{src}' normally drives '{relation}', "
f"which leads to '{tgt}'.\n"
f"Step 2 - Counterfactual intervention: if '{src}' is removed, the "
f"'{relation}' signal is attenuated or eliminated.\n"
f"Step 3 - Downstream effect: without the '{relation}' driver, "
f"the manifestation of '{tgt}' would be reduced or delayed.\n"
f"Step 4 - Caveats: redundant pathways may partially compensate; "
"the effect size depends on whether this is the primary upstream driver.\n"
f"Step 5 - Conclusion: treating or removing '{src}' is predicted to "
f"attenuate '{tgt}' via the '{relation}' mechanism.\n"
"</think>"
)
examples.append({
"task_type": "counterfactual_reasoning",
"instruction": (
"Think step-by-step. "
"Given a counterfactual scenario: if a peripheral factor were absent "
"or successfully treated, reason through what would happen to the "
"downstream brain outcome. "
"Show your reasoning inside <think>...</think> tags, then respond "
"with JSON: {\"effect\": ..., \"mechanism\": ..., \"caveats\": ...}."
),
"input": (
f"Counterfactual: if '{src}' were absent or successfully treated, "
f"what would happen to '{tgt}'?"
),
"output": (
cf_think + "\n"
+ json.dumps({
"effect": f"'{tgt}' would be reduced or delayed",
"mechanism": f"removal of '{src}' attenuates the '{relation}' pathway",
"caveats": "redundant upstream pathways may partially compensate",
})
),
"systemic_tags": tags,
"paper_id": paper_id,
"provenance": provenance,
})
paper_reasoning_count += 1
# ── Task C: multi_hop_chain ──────────────────────────────────────
# Find a second edge B->C where B == tgt (current edge A->B).
# Generate a two-hop chain question: "given only A, trace to C."
for next_relation, next_tgt in edge_index.get(tgt, []):
if paper_reasoning_count >= _MAX_EXAMPLES_PER_PAPER:
break
hop_think = (
"<think>\n"
f"Multi-hop chain: '{src}' -> '{tgt}' -> '{next_tgt}'.\n"
f"Step 1 - First hop: '{src}' drives '{relation}', leading to '{tgt}'.\n"
f"Step 2 - Second hop: '{tgt}', now acting as a secondary driver, "
f"drives '{next_relation}', leading to '{next_tgt}'.\n"
f"Step 3 - Chain composition: the net effect of '{src}' on '{next_tgt}' "
f"is mediated sequentially through '{tgt}'.\n"
f"Step 4 - Direct vs. indirect: this is an indirect effect; "
f"there may be no direct single-step edge from '{src}' to '{next_tgt}'.\n"
f"Step 5 - Conclusion: '{src}' reaches '{next_tgt}' indirectly via "
f"'{tgt}', through '{relation}' then '{next_relation}'.\n"
"</think>"
)
examples.append({
"task_type": "multi_hop_chain",
"instruction": (
"Think step-by-step. "
"Given a starting peripheral factor, trace a two-step biological "
"chain to identify the final downstream brain outcome in "
"Alzheimer's disease. "
"Show your reasoning inside <think>...</think> tags, then respond "
"with JSON: {\"intermediate\": ..., \"final_outcome\": ..., "
"\"step1_mechanism\": ..., \"step2_mechanism\": ...}."
),
"input": (
f"Starting factor: '{src}'. "
f"What is the downstream brain outcome after two biological steps?"
),
"output": (
hop_think + "\n"
+ json.dumps({
"intermediate": tgt,
"final_outcome": next_tgt,
"step1_mechanism": relation,
"step2_mechanism": next_relation,
})
),
"systemic_tags": tags,
"paper_id": paper_id,
"provenance": provenance,
})
paper_reasoning_count += 1
break # one multi-hop per originating edge is sufficient
return examples
def _therapeutic_reasoning_examples(extractions: list) -> list:
"""Generate therapeutic reasoning training examples.
These force the model to think like a drug designer, not a data retriever.
The model must reason about:
- What drug targets could address a mechanism
- What barriers exist to targeting a pathway
- What side effects might arise from intervention
- How to design experiments to test hypotheses
"""
import random as _rand
examples = []
paper_count = {}
for ext in extractions:
fields = ext.get("fields", {})
maps = fields.get("maps", []) or []
hypothesis = fields.get("hypothesis", "")
results = fields.get("results", "")
paper_id = ext.get("paper_id", "unknown")
if paper_count.get(paper_id, 0) >= _MAX_EXAMPLES_PER_PAPER:
continue
if not maps:
continue
for m in maps:
src = m.get("from", "")
tgt = m.get("to", "")
relation = m.get("relation", "relates_to")
if not src or not tgt:
continue
if paper_count.get(paper_id, 0) >= _MAX_EXAMPLES_PER_PAPER:
break
# Task: Therapeutic target identification
think = (
"<think>\n"
f"The mechanism linking {src} to {tgt} in Alzheimer's disease "
f"involves '{relation}'. To intervene therapeutically, I need to consider "
f"where in this pathway a drug could act.\n\n"
f"First, what is the biological nature of this pathway? The '{relation}' "
f"mechanism connects a peripheral factor to a central brain outcome. "
f"This means there are multiple potential intervention points: "
f"reducing the peripheral driver, blocking the transport/signaling mechanism, "
f"or protecting the brain from downstream effects.\n\n"
f"What are the barriers? The blood-brain barrier limits direct CNS access. "
f"Systemic interventions may have off-target effects. The timing of intervention "
f"matters — early intervention before neurodegeneration may be more effective.\n\n"
f"What drug modalities could work? Antibodies targeting the peripheral factor, "
f"small molecules blocking the signaling pathway, or gene therapies modifying "
f"the expression of key mediators.\n\n"
f"The most promising approach depends on the specificity of the '{relation}' "
f"mechanism and whether it is a primary driver or a compensatory pathway.\n"
"</think>"
)
examples.append({
"task_type": "therapeutic_reasoning",
"instruction": (
"You are a neuropharmacologist designing a therapeutic strategy for "
"Alzheimer's disease. Analyze the given mechanism and reason through "
"potential drug targets, delivery challenges, and experimental approaches. "
"Show your reasoning in <think>...</think>, then respond with analysis."
),
"input": (
f"Mechanism: {src} drives '{relation}', leading to {tgt}. "
f"What are the therapeutic implications and drug design considerations?"
),
"output": (
think + "\n"
+ f"The therapeutic strategy for targeting the '{relation}' pathway "
f"between {src} and {tgt} should focus on: (1) identifying the most "
f"druggable node in the pathway, (2) developing delivery mechanisms "
f"that cross the blood-brain barrier where needed, and "
f"(3) designing biomarkers to monitor treatment response. "
f"The key challenge is that systemic interventions targeting {src} "
f"may have unintended consequences on other physiological processes."
),
"paper_id": paper_id,
})
paper_count[paper_id] = paper_count.get(paper_id, 0) + 1
# Task: Experimental design reasoning
think2 = (
"<think>\n"
f"To test whether {src} causally contributes to {tgt} via '{relation}', "
f"I need to design an experiment that isolates this specific pathway.\n\n"
f"A correlation study alone is insufficient — I need interventional evidence. "
f"Options include: (1) a knockout model where the {src} pathway is disabled, "
f"(2) an antibody blockade study reducing circulating {src}, or "
f"(3) a longitudinal cohort tracking {src} levels and {tgt} progression.\n\n"
f"Each approach has tradeoffs. Knockout models prove causality but may not "
f"translate to human therapy. Antibody blockade is more translatable but "
f"requires knowing the right timing window. Longitudinal studies establish "
f"temporal relationships but cannot prove mechanism.\n\n"
f"The strongest evidence would combine approaches: use the longitudinal data "
f"to identify the critical time window, then use the interventional studies "
f"to test causality within that window.\n"
"</think>"
)
examples.append({
"task_type": "experimental_design",
"instruction": (
"Design an experimental approach to test whether a peripheral factor "
"causally contributes to brain pathology in Alzheimer's disease. "
"Reason through the experimental design in <think>...</think>, "
"then describe your recommended approach."
),
"input": (
f"Hypothesis: {src} contributes to {tgt} via '{relation}'. "
f"Design an experiment to test this."
),
"output": (
think2 + "\n"
+ f"To test the causal link between {src} and {tgt}, I recommend "
f"a multi-pronged approach: first establish the temporal relationship "
f"through longitudinal biomarker tracking, then use targeted intervention "
f"(antibody blockade or pathway inhibition) to test whether reducing "
f"the {src} signal attenuates {tgt} progression. Controls should include "
f"pathway-specific inhibitors and sham-treated animals."
),
"paper_id": paper_id,
})
paper_count[paper_id] = paper_count.get(paper_id, 0) + 1
return examples
def _general_examples(records: list) -> list:
examples = []
for rec in records:
params = rec.get("parameters", {})
if not params:
continue
examples.append({
"task_type": "general_context",
"instruction": (
"Given standard demographic/clinical parameters, assess Alzheimer's disease "
"risk context. Respond as JSON with key 'assessment'."
),
"input": json.dumps(params),
"output": json.dumps({"assessment": "", "species_class": rec.get("species_class")}),
"species_class": rec.get("species_class"),
"dataset_name": rec.get("dataset_name"),
})
return examples
def _maps_examples(maps: list) -> list:
examples = []
for m in maps:
if not (m.get("from") and m.get("to")):
continue
examples.append({
"task_type": "map_forward",
"instruction": "Predict the downstream node. Respond as JSON with key 'to'.",
"input": json.dumps({"from": m["from"], "relation": m.get("relation")}),
"output": json.dumps({"to": m["to"]}),
"domain": m.get("domain", ""),
})
examples.append({
"task_type": "map_reverse",
"instruction": "Reverse-engineer the upstream node. Respond as JSON with key 'from'.",
"input": json.dumps({"to": m["to"], "relation": m.get("relation")}),
"output": json.dumps({"from": m["from"]}),
"domain": m.get("domain", ""),
})
return examples
async def _gather_examples(kind: str) -> list:
if kind == "systemic":
extractions = await store.list_all("extractions", newest_first=True)
return _systemic_examples(extractions)
if kind == "general":
records = await store.list_all("general_records", newest_first=True)
return _general_examples(records)
if kind == "maps":
maps = await store.list_all("maps", newest_first=True)
return _maps_examples(maps)
if kind == "reasoning":
# Reasoning examples are derived from the same extractions as systemic
# examples. They deliberately overlap in source data but teach a
# completely different cognitive skill: structured inference rather than
# pattern-matched retrieval.
extractions = await store.list_all("extractions", newest_first=True)
return _reasoning_examples(extractions)
if kind == "therapeutic":
# Therapeutic reasoning teaches the model to think like a drug designer:
# analyzing mechanisms, identifying targets, designing experiments.
extractions = await store.list_all("extractions", newest_first=True)
return _therapeutic_reasoning_examples(extractions)
if kind == "all_reasoning":
# Combined reasoning + therapeutic examples for comprehensive training.
extractions = await store.list_all("extractions", newest_first=True)
return _reasoning_examples(extractions) + _therapeutic_reasoning_examples(extractions)
return []
# ─── Normalization ───────────────────────────────────────────────
_CANONICAL_KEYS = ("task_type", "instruction", "input", "output",
"systemic_tags", "paper_id", "provenance",
"species_class", "dataset_name", "domain",
# reasoning examples carry their CoT inside output; no extra keys needed
)
def _normalize_example(ex: dict) -> dict:
out = {}
for k in _CANONICAL_KEYS:
out[k] = ex.get(k, "" if k != "systemic_tags" else [])
return out
def _to_jsonl(examples: list) -> str:
return "\n".join(json.dumps(_normalize_example(ex), ensure_ascii=False) for ex in examples)
def parse_jsonl(jsonl: str) -> list:
examples = []
for line in jsonl.strip().split("\n"):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
if isinstance(obj, dict):
examples.append(obj)
except (json.JSONDecodeError, ValueError):
pass
return examples
def merge_jsonl_parts(parts: list) -> str:
"""Merge multiple JSONL strings, deduplicating by (task_type, input) pair."""
all_examples = []
for part in parts:
all_examples.extend(parse_jsonl(part))
seen = set()
unique = []
for ex in all_examples:
key = (ex.get("task_type", ""), ex.get("input", ""))
if key not in seen:
seen.add(key)
unique.append(ex)
return _to_jsonl(unique)
# ─── HF Hub operations ───────────────────────────────────────────
def _hf_token():
return os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
def _hf_repo():
return os.getenv("HF_DATASET_REPO")
def _push_to_hf(kind: str, version: str, jsonl: str) -> dict:
token = _hf_token()
repo = _hf_repo()
if not token or not repo:
return {"pushed": False, "reason": "HF_TOKEN or HF_DATASET_REPO not set"}
try:
from huggingface_hub import HfApi
api = HfApi(token=token)
api.create_repo(repo, repo_type="dataset", exist_ok=True, private=True)
path_in_repo = f"{kind}/{version}/training_data.jsonl"
api.upload_file(
path_or_fileobj=io.BytesIO(jsonl.encode("utf-8")),
path_in_repo=path_in_repo,
repo_id=repo,
repo_type="dataset",
commit_message=f"NeuroJenML {kind} dataset {version}",
)
return {"pushed": True, "repo": repo, "path": path_in_repo}
except Exception as e:
return {"pushed": False, "reason": str(e)[:300]}
def pull_from_hf(path_in_repo: str, max_retries: int = 3) -> str:
"""Pull a JSONL file from HF dataset repo by path. Retries on transient failures."""
token = _hf_token()
repo = _hf_repo()
if not token or not repo:
raise RuntimeError("HF_TOKEN or HF_DATASET_REPO not set")
from huggingface_hub import hf_hub_download
last_error = None
for attempt in range(max_retries):
try:
local_path = hf_hub_download(
repo_id=repo,
filename=path_in_repo,
repo_type="dataset",
token=token,
)
with open(local_path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
last_error = e
if attempt < max_retries - 1:
import time as _time
delay = 2 ** attempt
print(f"[retry] HF pull attempt {attempt + 1}/{max_retries} failed: {str(e)[:100]}. Retrying in {delay}s...")
_time.sleep(delay)
raise last_error
def push_extraction_to_hf(paper_id: str, extraction: dict, repo: str, token: str) -> str:
try:
from huggingface_hub import HfApi
except ImportError:
raise RuntimeError("huggingface_hub not installed")
import json as _json
record = {
"paper_id": paper_id,
"model": extraction.get("model"),
"classification": extraction.get("classification"),
"fields": extraction.get("fields"),
"provenance": extraction.get("provenance"),
"extracted_at": datetime.now().isoformat(),
}
content = (_json.dumps(record, ensure_ascii=False) + "\n").encode("utf-8")
path_in_repo = f"extractions/{paper_id}.jsonl"
api = HfApi(token=token)
api.upload_file(
path_or_fileobj=content,
path_in_repo=path_in_repo,
repo_id=repo,
repo_type="dataset",
commit_message=f"add extraction {paper_id}",
)
return f"https://huggingface.co/datasets/{repo}/blob/main/{path_in_repo}"
# ─── Build & Store (manifest only, JSONL goes to HF) ─────────────
async def build_and_record(kind: str, push: bool = True) -> dict:
"""Build a dataset, push JSONL to HF, store manifest in Neon."""
examples = await _gather_examples(kind)
version = "v" + datetime.now().strftime("%Y%m%d-%H%M%S")
jsonl = _to_jsonl(examples)
push_meta = {"pushed": False}
if push:
push_meta = _push_to_hf(kind, version, jsonl)
ds_id = f"ds-{kind}-{int(time.time() * 1000) % 10_000_000}"
manifest = {
"id": ds_id,
"kind": kind,
"version": version,
"example_count": len(examples),
"hf_path": push_meta.get("path", ""),
"hf_repo": push_meta.get("repo", ""),
"hf_pushed": push_meta.get("pushed", False),
# Inline JSONL stored as fallback when HF push is unavailable.
# This is what the training bridge uses when hf_path is empty.
# Capped at 8 MB to stay within Neon JSONB limits.
"jsonl_inline": jsonl if len(jsonl) < 8 * 1024 * 1024 else "",
"source_papers": list({ex.get("paper_id") for ex in examples if ex.get("paper_id")}),
"task_types": list({ex.get("task_type") for ex in examples if ex.get("task_type")}),
"used_in_training": False,
"training_job_id": None,
"created_at": datetime.now().isoformat(),
}
await store.upsert("datasets", ds_id, manifest)
return {
"status": "ok",
"dataset": manifest,
"jsonl": jsonl,
"count": len(examples),
}
# ─── Dataset Operations ──────────────────────────────────────────
async def list_datasets(kind: str = None, unused_only: bool = False) -> list:
filters = {}
if kind:
filters["kind"] = kind
datasets = await store.query("datasets", filters, newest_first=True)
if unused_only:
datasets = [d for d in datasets if not d.get("used_in_training")]
return datasets
async def get_dataset(ds_id: str) -> dict:
return await store.get("datasets", ds_id) or {}
async def get_dataset_jsonl(ds_id: str) -> str:
"""Return the JSONL for a dataset, pulling from HF or falling back to inline.
Priority:
1. HF Hub (hf_path set and HF credentials available)
2. Inline JSONL stored in the manifest at build time (jsonl_inline field)
"""
ds = await store.get("datasets", ds_id)
if not ds:
return ""
hf_path = ds.get("hf_path", "")
if hf_path:
try:
return await asyncio.to_thread(pull_from_hf, hf_path)
except Exception:
pass # fall through to inline
return ds.get("jsonl_inline", "")
async def merge_datasets(ds_ids: list, name: str = None) -> dict:
"""Merge multiple datasets by pulling each from HF and combining."""
all_examples = []
source_ids = []
source_papers = set()
for ds_id in ds_ids:
ds = await store.get("datasets", ds_id)
if not ds:
continue
hf_path = ds.get("hf_path", "")
if not hf_path:
continue # Skip datasets without HF path
try:
jsonl = await asyncio.to_thread(pull_from_hf, hf_path)
except Exception:
jsonl = ""
if not jsonl:
continue
examples = parse_jsonl(jsonl)
all_examples.extend(examples)
source_ids.append(ds_id)
source_papers.update(ds.get("source_papers", []))
# Deduplicate by (task_type, input)
seen = set()
unique = []
for ex in all_examples:
key = (ex.get("task_type", ""), ex.get("input", ""))
if key not in seen:
seen.add(key)
unique.append(ex)
version = "v" + datetime.now().strftime("%Y%m%d-%H%M%S")
jsonl = _to_jsonl(unique)
# Push merged dataset to HF
push_meta = _push_to_hf("merged", version, jsonl)
ds_id = f"ds-merged-{int(time.time() * 1000) % 10_000_000}"
manifest = {
"id": ds_id,
"kind": "merged",
"version": version,
"name": name or f"Merged from {len(source_ids)} datasets",
"example_count": len(unique),
"hf_path": push_meta.get("path", ""),
"hf_repo": push_meta.get("repo", ""),
"hf_pushed": push_meta.get("pushed", False),
"source_datasets": source_ids,
"source_papers": list(source_papers),
"task_types": list({ex.get("task_type") for ex in unique if ex.get("task_type")}),
"jsonl_inline": jsonl if len(jsonl) < 8 * 1024 * 1024 else "",
"used_in_training": False,
"training_job_id": None,
"created_at": datetime.now().isoformat(),
}
await store.upsert("datasets", ds_id, manifest)
return {
"status": "ok",
"dataset": manifest,
"count": len(unique),
"deduplicated": len(all_examples) - len(unique),
}
async def validate_dataset(ds_id: str) -> dict:
"""Validate a dataset pulled from HF. Checks structure and content quality."""
try:
ds = await store.get("datasets", ds_id)
jsonl = await asyncio.to_thread(
pull_from_hf, (ds or {}).get("hf_path", "")
)
except Exception as e:
return {"valid": False, "error": f"Could not pull from HF: {str(e)[:200]}"}
if not jsonl or not jsonl.strip():
return {"valid": False, "error": "Dataset is empty"}
examples = parse_jsonl(jsonl)
issues = []
required_keys = {"task_type", "instruction", "input", "output"}
seen_pairs = set()
leakage_count = 0
empty_fields = 0
for i, ex in enumerate(examples):
missing = required_keys - set(ex.keys())
if missing:
issues.append(f"Line {i + 1}: missing keys {missing}")
for key in ["instruction", "input", "output"]:
val = ex.get(key, "")
if not val or (isinstance(val, str) and not val.strip()):
empty_fields += 1
pair = (ex.get("task_type", ""), ex.get("input", ""))
if pair in seen_pairs:
issues.append(f"Line {i + 1}: duplicate (task_type, input) pair")
seen_pairs.add(pair)
output_val = ex.get("output", "")
input_val = ex.get("input", "")
if isinstance(output_val, str) and isinstance(input_val, str):
try:
out_obj = json.loads(output_val)
for v in out_obj.values():
if isinstance(v, str) and len(v) > 5 and v.lower() in input_val.lower():
leakage_count += 1
break
except (json.JSONDecodeError, ValueError):
pass
total = len(examples)
quality_score = 100.0
if total == 0:
quality_score = 0
else:
quality_score -= (empty_fields / max(1, total * 3)) * 30
quality_score -= (leakage_count / total) * 40
quality_score -= min(20, len(issues) * 2)
quality_score = max(0, round(quality_score, 1))
return {
"valid": len(issues) == 0 and total > 0,
"example_count": total,
"quality_score": quality_score,
"issues": issues[:20],
"issue_count": len(issues),
"empty_fields": empty_fields,
"leakage_examples": leakage_count,
"duplicate_pairs": len(examples) - len(seen_pairs),
}
async def mark_used(ds_id: str, job_id: str) -> bool:
ds = await store.get("datasets", ds_id)
if not ds:
return False
ds["used_in_training"] = True
ds["training_job_id"] = job_id
ds["used_at"] = datetime.now().isoformat()
await store.upsert("datasets", ds_id, ds)
return True
async def mark_unused(ds_id: str) -> bool:
"""Allow a used dataset to be reused for training."""
ds = await store.get("datasets", ds_id)
if not ds:
return False
ds["used_in_training"] = False
ds["reused"] = True
ds["reused_at"] = datetime.now().isoformat()
await store.upsert("datasets", ds_id, ds)
return True