Add reflective agent loop and hidden gem evidence
Browse files- README.md +15 -3
- backend/agent.py +335 -28
- frontend/dist/assets/index-BsUEW0Da.css +1 -0
- frontend/dist/assets/index-D64Ptpvl.js +0 -0
- frontend/dist/index.html +2 -2
- frontend/src/components/CandidateBoard.jsx +24 -8
- frontend/src/components/ResearchWorkspace.jsx +1 -0
- frontend/src/hud/AgentLog.jsx +9 -0
- frontend/src/hud/DetailCard.jsx +27 -0
- frontend/src/styles.css +70 -7
- tests/test_agent.py +62 -0
README.md
CHANGED
|
@@ -42,9 +42,11 @@ The app performs a multi-step research loop:
|
|
| 42 |
2. Plan multiple targeted Hub searches.
|
| 43 |
3. Deduplicate and pre-rank candidates by request relevance.
|
| 44 |
4. Inspect dataset cards, tags, configurations, splits, schema fields, and sample rows.
|
| 45 |
-
5.
|
| 46 |
-
6.
|
| 47 |
-
7.
|
|
|
|
|
|
|
| 48 |
|
| 49 |
`HuggingFaceTB/SmolLM2-360M-Instruct` runs locally inside the Space CPU runtime and helps interpret
|
| 50 |
the brief. At only 360M parameters, it is comfortably below the Tiny Titan 4B limit. The model
|
|
@@ -70,6 +72,16 @@ Missing evidence is shown as `unknown` rather than silently converted into an av
|
|
| 70 |
Hard modality, language, accessibility, or required-schema mismatches produce explicit rejection
|
| 71 |
reasons.
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
## Architecture
|
| 74 |
|
| 75 |
- **Agent:** Python, `huggingface_hub`, Hub API, and Dataset Viewer API
|
|
|
|
| 42 |
2. Plan multiple targeted Hub searches.
|
| 43 |
3. Deduplicate and pre-rank candidates by request relevance.
|
| 44 |
4. Inspect dataset cards, tags, configurations, splits, schema fields, and sample rows.
|
| 45 |
+
5. Reflect on first-pass failures and run a second targeted search when the evidence is weak.
|
| 46 |
+
6. Run explicit modality, language, required-field, sample-row, license, and accessibility checks.
|
| 47 |
+
7. Highlight hidden gems when low-adoption datasets have strong verified fit.
|
| 48 |
+
8. Rank candidates from evidence and connect potentially complementary datasets.
|
| 49 |
+
9. Stream the trace and explain verified strengths, limitations, and rejection reasons.
|
| 50 |
|
| 51 |
`HuggingFaceTB/SmolLM2-360M-Instruct` runs locally inside the Space CPU runtime and helps interpret
|
| 52 |
the brief. At only 360M parameters, it is comfortably below the Tiny Titan 4B limit. The model
|
|
|
|
| 72 |
Hard modality, language, accessibility, or required-schema mismatches produce explicit rejection
|
| 73 |
reasons.
|
| 74 |
|
| 75 |
+
## Agent loop
|
| 76 |
+
|
| 77 |
+
HF Agentic Search does not stop after one keyword pass. It runs an initial search plan, inspects
|
| 78 |
+
real Hugging Face evidence, reflects on what failed, and launches a bounded second-pass search
|
| 79 |
+
aimed at the missing signal. For example, if the first climate search finds reports but not
|
| 80 |
+
question-answer rows, the agent switches to schema-first queries such as `climate qa dataset`.
|
| 81 |
+
|
| 82 |
+
The final result includes visible trace events, sample-row tests, hidden-gem labels, rejection
|
| 83 |
+
reasons, and starter `load_dataset` code for the selected dataset.
|
| 84 |
+
|
| 85 |
## Architecture
|
| 86 |
|
| 87 |
- **Agent:** Python, `huggingface_hub`, Hub API, and Dataset Viewer API
|
backend/agent.py
CHANGED
|
@@ -452,6 +452,122 @@ def _matches_field(requirement: str, field: str) -> bool:
|
|
| 452 |
return normalized in aliases
|
| 453 |
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
def score_dataset(profile: dict[str, Any], dataset: dict[str, Any]) -> dict[str, Any]:
|
| 456 |
"""Compute a transparent score entirely from collected evidence."""
|
| 457 |
blob = _text_blob(dataset)
|
|
@@ -546,6 +662,10 @@ def score_dataset(profile: dict[str, Any], dataset: dict[str, Any]) -> dict[str,
|
|
| 546 |
})
|
| 547 |
schema_evidence = "missing" if available_fields else "unknown"
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
license_value = dataset.get("license", "")
|
| 550 |
permissive = {"apache-2.0", "mit", "cc-by-4.0", "cc0-1.0", "odc-by", "bsd-3-clause"}
|
| 551 |
license_score = 10 if license_value in permissive else 5 if license_value else 0
|
|
@@ -668,6 +788,20 @@ def score_dataset(profile: dict[str, Any], dataset: dict[str, Any]) -> dict[str,
|
|
| 668 |
quality = round(
|
| 669 |
(schema + license_score + documentation + popularity + accessibility) / 40 * 100
|
| 670 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
return {
|
| 672 |
**dataset,
|
| 673 |
"score": total,
|
|
@@ -687,12 +821,17 @@ def score_dataset(profile: dict[str, Any], dataset: dict[str, Any]) -> dict[str,
|
|
| 687 |
"accessibility": accessibility,
|
| 688 |
},
|
| 689 |
"checks": checks,
|
|
|
|
|
|
|
| 690 |
"schema_evidence": schema_evidence,
|
| 691 |
"evidence": evidence,
|
| 692 |
"rejection_reasons": rejection_reasons,
|
| 693 |
"strength": strength,
|
| 694 |
"weakness": weakness,
|
| 695 |
"recommendation": recommendation,
|
|
|
|
|
|
|
|
|
|
| 696 |
}
|
| 697 |
|
| 698 |
|
|
@@ -714,16 +853,115 @@ def _cross_reference(datasets: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
| 714 |
return pairs
|
| 715 |
|
| 716 |
|
| 717 |
-
def _rank_key(profile: dict[str, Any], dataset: dict[str, Any]) -> tuple[int, int, int]:
|
| 718 |
checks = dataset["checks"]
|
|
|
|
|
|
|
| 719 |
evidence_fit = (
|
| 720 |
(2 if checks["required_fields"] == "pass" else 0)
|
| 721 |
+ (1 if checks["domain"] == "pass" else 0)
|
| 722 |
+ (2 if profile["languages"] and checks["language"] == "pass" else 0)
|
| 723 |
+ (2 if profile["license"] and checks["license"] == "pass" else 0)
|
|
|
|
| 724 |
)
|
| 725 |
status_rank = {"recommended": 2, "conditional": 1, "rejected": 0}[dataset["status"]]
|
| 726 |
-
return status_rank, evidence_fit, dataset["score"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
|
| 729 |
def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
@@ -748,7 +986,13 @@ def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
| 748 |
|
| 749 |
collected: dict[str, dict[str, Any]] = {}
|
| 750 |
search_batches: list[list[str]] = []
|
| 751 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
found = search_datasets(query, limit=35)
|
| 753 |
search_batches.append([dataset["id"] for dataset in found])
|
| 754 |
for dataset in found:
|
|
@@ -764,37 +1008,20 @@ def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
| 764 |
}
|
| 765 |
|
| 766 |
inspection_limit = max(max_datasets * 4, 28)
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
|
|
|
|
|
|
| 771 |
)
|
| 772 |
-
diversified_ids: list[str] = []
|
| 773 |
-
diversified_seen: set[str] = set()
|
| 774 |
-
for position in range(8):
|
| 775 |
-
for batch in search_batches:
|
| 776 |
-
if position >= len(batch):
|
| 777 |
-
continue
|
| 778 |
-
dataset_id = batch[position]
|
| 779 |
-
if dataset_id not in diversified_seen:
|
| 780 |
-
diversified_seen.add(dataset_id)
|
| 781 |
-
diversified_ids.append(dataset_id)
|
| 782 |
-
for dataset in global_ranked:
|
| 783 |
-
if dataset["id"] not in diversified_seen:
|
| 784 |
-
diversified_ids.append(dataset["id"])
|
| 785 |
-
pre_ranked = [
|
| 786 |
-
collected[dataset_id]
|
| 787 |
-
for dataset_id in diversified_ids[:inspection_limit]
|
| 788 |
-
if dataset_id in collected
|
| 789 |
-
]
|
| 790 |
yield {
|
| 791 |
"type": "search",
|
| 792 |
-
"query": "
|
| 793 |
"found": len(pre_ranked),
|
| 794 |
"unique": len(collected),
|
| 795 |
-
"message": f"Prepared {len(pre_ranked)} diverse candidates for
|
| 796 |
}
|
| 797 |
-
inspected: list[dict[str, Any]] = []
|
| 798 |
with ThreadPoolExecutor(max_workers=min(4, max(1, len(pre_ranked)))) as pool:
|
| 799 |
futures = {
|
| 800 |
pool.submit(inspect_dataset, dataset["id"], dataset): dataset["id"]
|
|
@@ -816,6 +1043,7 @@ def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
| 816 |
}
|
| 817 |
scored = score_dataset(profile, evidence)
|
| 818 |
inspected.append(scored)
|
|
|
|
| 819 |
yield {
|
| 820 |
"type": "inspect",
|
| 821 |
"dataset_id": dataset_id,
|
|
@@ -826,6 +1054,83 @@ def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
| 826 |
}
|
| 827 |
yield {"type": "candidate", "dataset": _public_dataset(scored)}
|
| 828 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
ranked = sorted(
|
| 830 |
inspected,
|
| 831 |
key=lambda dataset: _rank_key(profile, dataset),
|
|
@@ -856,6 +1161,7 @@ def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
| 856 |
"model_used": MODEL if llm_used else None,
|
| 857 |
"fallback_used": not llm_used,
|
| 858 |
"elapsed_ms": round((time.time() - started) * 1000),
|
|
|
|
| 859 |
}
|
| 860 |
yield {
|
| 861 |
"type": "ranking",
|
|
@@ -874,6 +1180,7 @@ def _public_dataset(dataset: dict[str, Any]) -> dict[str, Any]:
|
|
| 874 |
"accessible", "inspection_error", "card_complete", "num_examples", "score", "relevance",
|
| 875 |
"quality", "status", "score_breakdown", "checks", "evidence",
|
| 876 |
"schema_evidence", "rejection_reasons", "strength", "weakness", "recommendation",
|
|
|
|
| 877 |
}
|
| 878 |
return {key: value for key, value in dataset.items() if key in allowed}
|
| 879 |
|
|
|
|
| 452 |
return normalized in aliases
|
| 453 |
|
| 454 |
|
| 455 |
+
def _field_value(row: dict[str, Any], fields: list[str]) -> Any:
|
| 456 |
+
lowered = {str(key).lower(): value for key, value in row.items()}
|
| 457 |
+
for field in fields:
|
| 458 |
+
value = lowered.get(str(field).lower())
|
| 459 |
+
if value not in (None, "", [], {}):
|
| 460 |
+
return value
|
| 461 |
+
return None
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _sample_tests(
|
| 465 |
+
profile: dict[str, Any],
|
| 466 |
+
dataset: dict[str, Any],
|
| 467 |
+
matched_requirements: dict[str, list[str]],
|
| 468 |
+
) -> list[dict[str, str]]:
|
| 469 |
+
rows = [row for row in dataset.get("sample_rows", []) if isinstance(row, dict)]
|
| 470 |
+
required_fields = profile["required_fields"]
|
| 471 |
+
tests: list[dict[str, str]] = []
|
| 472 |
+
if not rows:
|
| 473 |
+
return [{
|
| 474 |
+
"name": "Sample rows available",
|
| 475 |
+
"status": "unknown",
|
| 476 |
+
"detail": "Dataset Viewer did not expose sample rows.",
|
| 477 |
+
}]
|
| 478 |
+
|
| 479 |
+
missing = [
|
| 480 |
+
requirement for requirement in required_fields
|
| 481 |
+
if not matched_requirements.get(requirement)
|
| 482 |
+
]
|
| 483 |
+
empty = [
|
| 484 |
+
requirement for requirement in required_fields
|
| 485 |
+
if matched_requirements.get(requirement)
|
| 486 |
+
and not any(_field_value(row, matched_requirements[requirement]) is not None for row in rows)
|
| 487 |
+
]
|
| 488 |
+
tests.append({
|
| 489 |
+
"name": "Required fields populated",
|
| 490 |
+
"status": "pass" if not missing and not empty else "fail" if missing else "review",
|
| 491 |
+
"detail": "All requested fields have sample values."
|
| 492 |
+
if not missing and not empty
|
| 493 |
+
else f"Missing: {', '.join(missing or empty)}.",
|
| 494 |
+
})
|
| 495 |
+
|
| 496 |
+
if {"question", "answer"} & set(required_fields):
|
| 497 |
+
question_fields = matched_requirements.get("question", [])
|
| 498 |
+
answer_fields = matched_requirements.get("answer", [])
|
| 499 |
+
question_values = [
|
| 500 |
+
str(_field_value(row, question_fields) or "") for row in rows
|
| 501 |
+
if _field_value(row, question_fields) is not None
|
| 502 |
+
]
|
| 503 |
+
answer_values = [
|
| 504 |
+
str(_field_value(row, answer_fields) or "") for row in rows
|
| 505 |
+
if _field_value(row, answer_fields) is not None
|
| 506 |
+
]
|
| 507 |
+
plausible_questions = sum(
|
| 508 |
+
1 for value in question_values
|
| 509 |
+
if value.endswith("?") or len(value.split()) >= 3
|
| 510 |
+
)
|
| 511 |
+
distinct_answers = sum(
|
| 512 |
+
1 for question, answer in zip(question_values, answer_values)
|
| 513 |
+
if answer and answer.strip().lower() != question.strip().lower()
|
| 514 |
+
)
|
| 515 |
+
tests.append({
|
| 516 |
+
"name": "QA row shape",
|
| 517 |
+
"status": "pass" if plausible_questions and distinct_answers else "review",
|
| 518 |
+
"detail": "Questions and answers look usable in inspected rows."
|
| 519 |
+
if plausible_questions and distinct_answers
|
| 520 |
+
else "Question/answer shape needs manual review.",
|
| 521 |
+
})
|
| 522 |
+
|
| 523 |
+
if "label" in required_fields:
|
| 524 |
+
label_fields = matched_requirements.get("label", [])
|
| 525 |
+
label_values = [
|
| 526 |
+
_field_value(row, label_fields) for row in rows
|
| 527 |
+
if _field_value(row, label_fields) is not None
|
| 528 |
+
]
|
| 529 |
+
unique_labels = {str(value) for value in label_values}
|
| 530 |
+
tests.append({
|
| 531 |
+
"name": "Label signal",
|
| 532 |
+
"status": "pass" if label_values and len(unique_labels) <= max(20, len(rows)) else "review",
|
| 533 |
+
"detail": f"Found {len(unique_labels)} inspected label value(s)."
|
| 534 |
+
if label_values else "No label values were visible in sample rows.",
|
| 535 |
+
})
|
| 536 |
+
|
| 537 |
+
if {"document", "summary"}.issubset(required_fields):
|
| 538 |
+
document_fields = matched_requirements.get("document", [])
|
| 539 |
+
summary_fields = matched_requirements.get("summary", [])
|
| 540 |
+
shorter = 0
|
| 541 |
+
for row in rows:
|
| 542 |
+
document = str(_field_value(row, document_fields) or "")
|
| 543 |
+
summary = str(_field_value(row, summary_fields) or "")
|
| 544 |
+
if document and summary and len(summary) < len(document):
|
| 545 |
+
shorter += 1
|
| 546 |
+
tests.append({
|
| 547 |
+
"name": "Summary shape",
|
| 548 |
+
"status": "pass" if shorter else "review",
|
| 549 |
+
"detail": "Summaries are shorter than source documents in inspected rows."
|
| 550 |
+
if shorter else "Summary/document length relationship needs review.",
|
| 551 |
+
})
|
| 552 |
+
|
| 553 |
+
return tests
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def _loader_snippet(dataset: dict[str, Any]) -> str:
|
| 557 |
+
dataset_id = dataset.get("id", "")
|
| 558 |
+
config = (dataset.get("configs") or [None])[0]
|
| 559 |
+
split = (dataset.get("splits") or ["train"])[0]
|
| 560 |
+
args = f'"{dataset_id}"'
|
| 561 |
+
if config and config != "default":
|
| 562 |
+
args += f', "{config}"'
|
| 563 |
+
return (
|
| 564 |
+
"from datasets import load_dataset\n\n"
|
| 565 |
+
f'ds = load_dataset({args})\n'
|
| 566 |
+
f'rows = ds["{split}"]\n'
|
| 567 |
+
"print(rows[0])"
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
|
| 571 |
def score_dataset(profile: dict[str, Any], dataset: dict[str, Any]) -> dict[str, Any]:
|
| 572 |
"""Compute a transparent score entirely from collected evidence."""
|
| 573 |
blob = _text_blob(dataset)
|
|
|
|
| 662 |
})
|
| 663 |
schema_evidence = "missing" if available_fields else "unknown"
|
| 664 |
|
| 665 |
+
sample_tests = _sample_tests(profile, dataset, matched_requirements)
|
| 666 |
+
sample_test_passes = sum(1 for test in sample_tests if test["status"] == "pass")
|
| 667 |
+
sample_test_total = len(sample_tests)
|
| 668 |
+
|
| 669 |
license_value = dataset.get("license", "")
|
| 670 |
permissive = {"apache-2.0", "mit", "cc-by-4.0", "cc0-1.0", "odc-by", "bsd-3-clause"}
|
| 671 |
license_score = 10 if license_value in permissive else 5 if license_value else 0
|
|
|
|
| 788 |
quality = round(
|
| 789 |
(schema + license_score + documentation + popularity + accessibility) / 40 * 100
|
| 790 |
)
|
| 791 |
+
low_adoption = (dataset.get("downloads", 0) < 2_000 and dataset.get("likes", 0) < 25)
|
| 792 |
+
hidden_gem = (
|
| 793 |
+
status != "rejected"
|
| 794 |
+
and total >= 60
|
| 795 |
+
and low_adoption
|
| 796 |
+
and checks["domain"] == "pass"
|
| 797 |
+
and checks["required_fields"] in {"pass", "review"}
|
| 798 |
+
and checks["accessible"] == "pass"
|
| 799 |
+
)
|
| 800 |
+
badges = ["hidden_gem"] if hidden_gem else []
|
| 801 |
+
discovery_note = (
|
| 802 |
+
"Hidden gem: low adoption, but the inspected evidence fits this brief."
|
| 803 |
+
if hidden_gem else ""
|
| 804 |
+
)
|
| 805 |
return {
|
| 806 |
**dataset,
|
| 807 |
"score": total,
|
|
|
|
| 821 |
"accessibility": accessibility,
|
| 822 |
},
|
| 823 |
"checks": checks,
|
| 824 |
+
"sample_tests": sample_tests,
|
| 825 |
+
"sample_test_summary": f"{sample_test_passes}/{sample_test_total} sample tests passed",
|
| 826 |
"schema_evidence": schema_evidence,
|
| 827 |
"evidence": evidence,
|
| 828 |
"rejection_reasons": rejection_reasons,
|
| 829 |
"strength": strength,
|
| 830 |
"weakness": weakness,
|
| 831 |
"recommendation": recommendation,
|
| 832 |
+
"badges": badges,
|
| 833 |
+
"discovery_note": discovery_note,
|
| 834 |
+
"loader_snippet": _loader_snippet(dataset),
|
| 835 |
}
|
| 836 |
|
| 837 |
|
|
|
|
| 853 |
return pairs
|
| 854 |
|
| 855 |
|
| 856 |
+
def _rank_key(profile: dict[str, Any], dataset: dict[str, Any]) -> tuple[int, int, int, int]:
|
| 857 |
checks = dataset["checks"]
|
| 858 |
+
hidden_gem = 1 if "hidden_gem" in dataset.get("badges", []) else 0
|
| 859 |
+
sample_passes = sum(1 for test in dataset.get("sample_tests", []) if test.get("status") == "pass")
|
| 860 |
evidence_fit = (
|
| 861 |
(2 if checks["required_fields"] == "pass" else 0)
|
| 862 |
+ (1 if checks["domain"] == "pass" else 0)
|
| 863 |
+ (2 if profile["languages"] and checks["language"] == "pass" else 0)
|
| 864 |
+ (2 if profile["license"] and checks["license"] == "pass" else 0)
|
| 865 |
+
+ sample_passes
|
| 866 |
)
|
| 867 |
status_rank = {"recommended": 2, "conditional": 1, "rejected": 0}[dataset["status"]]
|
| 868 |
+
return status_rank, hidden_gem, evidence_fit, dataset["score"]
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
def _select_candidates_for_profile(
|
| 872 |
+
profile: dict[str, Any],
|
| 873 |
+
collected: dict[str, dict[str, Any]],
|
| 874 |
+
search_batches: list[list[str]],
|
| 875 |
+
limit: int,
|
| 876 |
+
) -> list[dict[str, Any]]:
|
| 877 |
+
global_ranked = sorted(
|
| 878 |
+
collected.values(),
|
| 879 |
+
key=lambda dataset: _pre_score(profile, dataset),
|
| 880 |
+
reverse=True,
|
| 881 |
+
)
|
| 882 |
+
diversified_ids: list[str] = []
|
| 883 |
+
diversified_seen: set[str] = set()
|
| 884 |
+
for position in range(8):
|
| 885 |
+
for batch in search_batches:
|
| 886 |
+
if position >= len(batch):
|
| 887 |
+
continue
|
| 888 |
+
dataset_id = batch[position]
|
| 889 |
+
if dataset_id not in diversified_seen:
|
| 890 |
+
diversified_seen.add(dataset_id)
|
| 891 |
+
diversified_ids.append(dataset_id)
|
| 892 |
+
for dataset in global_ranked:
|
| 893 |
+
if dataset["id"] not in diversified_seen:
|
| 894 |
+
diversified_ids.append(dataset["id"])
|
| 895 |
+
return [
|
| 896 |
+
collected[dataset_id]
|
| 897 |
+
for dataset_id in diversified_ids[:limit]
|
| 898 |
+
if dataset_id in collected
|
| 899 |
+
]
|
| 900 |
+
|
| 901 |
+
|
| 902 |
+
def _reflect_on_results(
|
| 903 |
+
profile: dict[str, Any],
|
| 904 |
+
tried_queries: list[str],
|
| 905 |
+
inspected: list[dict[str, Any]],
|
| 906 |
+
) -> dict[str, Any]:
|
| 907 |
+
if not inspected:
|
| 908 |
+
summary = "The first search did not produce inspectable datasets, so the agent is broadening the query language."
|
| 909 |
+
strategy = "broaden search"
|
| 910 |
+
else:
|
| 911 |
+
failures = {
|
| 912 |
+
key: sum(1 for dataset in inspected if dataset.get("checks", {}).get(key) in {"fail", "unknown"})
|
| 913 |
+
for key in ("domain", "required_fields", "language", "license", "accessible")
|
| 914 |
+
}
|
| 915 |
+
worst = max(failures, key=failures.get)
|
| 916 |
+
if worst == "required_fields":
|
| 917 |
+
summary = "The first pass found topical datasets, but too many missed the requested schema."
|
| 918 |
+
strategy = "schema-first search"
|
| 919 |
+
elif worst == "domain":
|
| 920 |
+
summary = "The first pass found task-shaped datasets, but several were off-topic."
|
| 921 |
+
strategy = "domain-first search"
|
| 922 |
+
elif worst == "language":
|
| 923 |
+
summary = "The first pass found candidates with weak language evidence."
|
| 924 |
+
strategy = "language-focused search"
|
| 925 |
+
else:
|
| 926 |
+
summary = "The first pass found partial fits; the agent is looking for less obvious alternatives."
|
| 927 |
+
strategy = "hidden-gem search"
|
| 928 |
+
|
| 929 |
+
terms = _domain_terms(profile)
|
| 930 |
+
domain = terms[:2]
|
| 931 |
+
required = profile["required_fields"]
|
| 932 |
+
next_queries: list[str] = []
|
| 933 |
+
if domain and {"question", "answer"}.issubset(required):
|
| 934 |
+
next_queries.extend([
|
| 935 |
+
f"{domain[0]} qa dataset",
|
| 936 |
+
" ".join(domain + ["question answer"]),
|
| 937 |
+
" ".join(domain + ["benchmark"]),
|
| 938 |
+
])
|
| 939 |
+
if domain and "label" in required:
|
| 940 |
+
next_queries.extend([
|
| 941 |
+
f"{domain[0]} labeled dataset",
|
| 942 |
+
f"{domain[0]} intent labels",
|
| 943 |
+
])
|
| 944 |
+
if domain and "summary" in required:
|
| 945 |
+
next_queries.extend([
|
| 946 |
+
" ".join(domain + ["summaries"]),
|
| 947 |
+
" ".join(domain + ["summarization dataset"]),
|
| 948 |
+
])
|
| 949 |
+
if profile["languages"] and domain:
|
| 950 |
+
next_queries.append(" ".join([profile["languages"][0], domain[0], "dataset"]))
|
| 951 |
+
if domain:
|
| 952 |
+
next_queries.append(" ".join(domain + ["data"]))
|
| 953 |
+
next_queries.extend(generate_queries("", profile))
|
| 954 |
+
cleaned = []
|
| 955 |
+
for query in next_queries:
|
| 956 |
+
normalized = re.sub(r"\s+", " ", query).strip()
|
| 957 |
+
if normalized and normalized not in tried_queries and normalized not in cleaned:
|
| 958 |
+
cleaned.append(normalized)
|
| 959 |
+
return {
|
| 960 |
+
"summary": summary,
|
| 961 |
+
"strategy": strategy,
|
| 962 |
+
"next_queries": cleaned[:4],
|
| 963 |
+
"reason": "Reflection is based on failed checks from the first inspected batch.",
|
| 964 |
+
}
|
| 965 |
|
| 966 |
|
| 967 |
def weave_events(task: str, max_datasets: int = 8) -> Iterator[dict[str, Any]]:
|
|
|
|
| 986 |
|
| 987 |
collected: dict[str, dict[str, Any]] = {}
|
| 988 |
search_batches: list[list[str]] = []
|
| 989 |
+
inspected: list[dict[str, Any]] = []
|
| 990 |
+
inspected_ids: set[str] = set()
|
| 991 |
+
|
| 992 |
+
first_pass_queries = queries[:5]
|
| 993 |
+
reserve_queries = queries[5:]
|
| 994 |
+
|
| 995 |
+
for query in first_pass_queries:
|
| 996 |
found = search_datasets(query, limit=35)
|
| 997 |
search_batches.append([dataset["id"] for dataset in found])
|
| 998 |
for dataset in found:
|
|
|
|
| 1008 |
}
|
| 1009 |
|
| 1010 |
inspection_limit = max(max_datasets * 4, 28)
|
| 1011 |
+
first_pass_limit = max(max_datasets * 2, 16)
|
| 1012 |
+
pre_ranked = _select_candidates_for_profile(
|
| 1013 |
+
profile,
|
| 1014 |
+
collected,
|
| 1015 |
+
search_batches,
|
| 1016 |
+
first_pass_limit,
|
| 1017 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
yield {
|
| 1019 |
"type": "search",
|
| 1020 |
+
"query": "first evidence pool",
|
| 1021 |
"found": len(pre_ranked),
|
| 1022 |
"unique": len(collected),
|
| 1023 |
+
"message": f"Prepared {len(pre_ranked)} diverse candidates for first-pass inspection.",
|
| 1024 |
}
|
|
|
|
| 1025 |
with ThreadPoolExecutor(max_workers=min(4, max(1, len(pre_ranked)))) as pool:
|
| 1026 |
futures = {
|
| 1027 |
pool.submit(inspect_dataset, dataset["id"], dataset): dataset["id"]
|
|
|
|
| 1043 |
}
|
| 1044 |
scored = score_dataset(profile, evidence)
|
| 1045 |
inspected.append(scored)
|
| 1046 |
+
inspected_ids.add(dataset_id)
|
| 1047 |
yield {
|
| 1048 |
"type": "inspect",
|
| 1049 |
"dataset_id": dataset_id,
|
|
|
|
| 1054 |
}
|
| 1055 |
yield {"type": "candidate", "dataset": _public_dataset(scored)}
|
| 1056 |
|
| 1057 |
+
reflection = _reflect_on_results(profile, first_pass_queries, inspected)
|
| 1058 |
+
second_pass_queries = list(dict.fromkeys(reflection["next_queries"] + reserve_queries))[:4]
|
| 1059 |
+
yield {
|
| 1060 |
+
"type": "reflect",
|
| 1061 |
+
"summary": reflection["summary"],
|
| 1062 |
+
"strategy": reflection["strategy"],
|
| 1063 |
+
"next_queries": second_pass_queries,
|
| 1064 |
+
"message": f"{reflection['summary']} Trying {len(second_pass_queries)} deeper search angle(s).",
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
for query in second_pass_queries:
|
| 1068 |
+
found = search_datasets(query, limit=35)
|
| 1069 |
+
search_batches.append([dataset["id"] for dataset in found])
|
| 1070 |
+
new_count = 0
|
| 1071 |
+
for dataset in found:
|
| 1072 |
+
current = collected.get(dataset["id"])
|
| 1073 |
+
if current is None:
|
| 1074 |
+
new_count += 1
|
| 1075 |
+
if current is None or _pre_score(profile, dataset) > _pre_score(profile, current):
|
| 1076 |
+
collected[dataset["id"]] = dataset
|
| 1077 |
+
yield {
|
| 1078 |
+
"type": "search",
|
| 1079 |
+
"query": query,
|
| 1080 |
+
"found": len(found),
|
| 1081 |
+
"unique": len(collected),
|
| 1082 |
+
"message": f"Deepened with “{query}” and found {len(found)} candidates ({new_count} new).",
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
deeper_pool = [
|
| 1086 |
+
dataset for dataset in _select_candidates_for_profile(
|
| 1087 |
+
profile,
|
| 1088 |
+
collected,
|
| 1089 |
+
search_batches,
|
| 1090 |
+
inspection_limit,
|
| 1091 |
+
)
|
| 1092 |
+
if dataset["id"] not in inspected_ids
|
| 1093 |
+
][:max(max_datasets * 2, 16)]
|
| 1094 |
+
yield {
|
| 1095 |
+
"type": "search",
|
| 1096 |
+
"query": "deep candidate pool",
|
| 1097 |
+
"found": len(deeper_pool),
|
| 1098 |
+
"unique": len(collected),
|
| 1099 |
+
"message": f"Prepared {len(deeper_pool)} fresh candidates after reflection.",
|
| 1100 |
+
}
|
| 1101 |
+
if deeper_pool:
|
| 1102 |
+
with ThreadPoolExecutor(max_workers=min(4, len(deeper_pool))) as pool:
|
| 1103 |
+
futures = {
|
| 1104 |
+
pool.submit(inspect_dataset, dataset["id"], dataset): dataset["id"]
|
| 1105 |
+
for dataset in deeper_pool
|
| 1106 |
+
}
|
| 1107 |
+
for future in as_completed(futures):
|
| 1108 |
+
dataset_id = futures[future]
|
| 1109 |
+
try:
|
| 1110 |
+
evidence = future.result()
|
| 1111 |
+
except Exception as exc:
|
| 1112 |
+
evidence = {
|
| 1113 |
+
**next(item for item in deeper_pool if item["id"] == dataset_id),
|
| 1114 |
+
"accessible": False,
|
| 1115 |
+
"inspection_error": str(exc),
|
| 1116 |
+
"features": [],
|
| 1117 |
+
"sample_rows": [],
|
| 1118 |
+
"configs": [],
|
| 1119 |
+
"splits": [],
|
| 1120 |
+
}
|
| 1121 |
+
scored = score_dataset(profile, evidence)
|
| 1122 |
+
inspected.append(scored)
|
| 1123 |
+
inspected_ids.add(dataset_id)
|
| 1124 |
+
yield {
|
| 1125 |
+
"type": "inspect",
|
| 1126 |
+
"dataset_id": dataset_id,
|
| 1127 |
+
"status": scored["status"],
|
| 1128 |
+
"score": scored["score"],
|
| 1129 |
+
"checks": scored["checks"],
|
| 1130 |
+
"message": f"Inspected {dataset_id}: {scored['status']} ({scored['score']}/100).",
|
| 1131 |
+
}
|
| 1132 |
+
yield {"type": "candidate", "dataset": _public_dataset(scored)}
|
| 1133 |
+
|
| 1134 |
ranked = sorted(
|
| 1135 |
inspected,
|
| 1136 |
key=lambda dataset: _rank_key(profile, dataset),
|
|
|
|
| 1161 |
"model_used": MODEL if llm_used else None,
|
| 1162 |
"fallback_used": not llm_used,
|
| 1163 |
"elapsed_ms": round((time.time() - started) * 1000),
|
| 1164 |
+
"reflection": reflection,
|
| 1165 |
}
|
| 1166 |
yield {
|
| 1167 |
"type": "ranking",
|
|
|
|
| 1180 |
"accessible", "inspection_error", "card_complete", "num_examples", "score", "relevance",
|
| 1181 |
"quality", "status", "score_breakdown", "checks", "evidence",
|
| 1182 |
"schema_evidence", "rejection_reasons", "strength", "weakness", "recommendation",
|
| 1183 |
+
"sample_tests", "sample_test_summary", "badges", "discovery_note", "loader_snippet",
|
| 1184 |
}
|
| 1185 |
return {key: value for key, value in dataset.items() if key in allowed}
|
| 1186 |
|
frontend/dist/assets/index-BsUEW0Da.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import"https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&family=Geist:wght@400;500;600;700&display=swap";:root{--paper: #f4faff;--paper-light: #ffffff;--ink: #102a43;--ink-soft: #2f465c;--line: #84b9df;--forest: #2489c9;--forest-dark: #12689d;--amber: #c47a18;--clay: #bd4b5c;--muted: #dceefa;--line-soft: #c6e0f2;--surface-blue: #e6f4fe;--surface-blue-strong: #d2ecfc;--font-display: "Geist", Arial, sans-serif;--font-body: "Geist", Arial, sans-serif;--font-mono: "Geist Mono", monospace;--radius-sm: 7px;--radius-md: 11px}*{box-sizing:border-box}html,body,#root{min-height:100%;margin:0}body{background:var(--paper);color:var(--ink);font-family:var(--font-body);-webkit-font-smoothing:antialiased}button,textarea{font:inherit}button,a{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible,textarea:focus-visible{outline:3px solid #62b5e8;outline-offset:2px}.app-shell{min-height:100vh;display:flex;flex-direction:column}.topbar{min-height:60px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);background:var(--paper-light)}.brand{display:inline-flex;align-items:center;color:var(--ink);font-family:var(--font-display);font-size:18px;font-weight:650;text-decoration:none}.topbar-meta{display:flex;align-items:center;gap:24px;font:12px var(--font-mono)}.run-state{padding:8px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-blue);text-transform:uppercase}.run-state.active{background:var(--surface-blue-strong);color:var(--forest-dark)}.workspace{flex:1;min-height:0;display:grid;grid-template-columns:minmax(285px,22vw) minmax(520px,1fr) minmax(315px,25vw);border-top:1px solid var(--line)}.rail{min-width:0;background:var(--paper-light);overflow-y:auto;max-height:none}.rail-left{border-right:1px solid var(--line)}.rail-right{border-left:1px solid var(--line)}.map-stage{min-width:0;min-height:760px;display:flex;flex-direction:column;background:linear-gradient(rgba(36,137,201,.055) 1px,transparent 1px),linear-gradient(90deg,rgba(36,137,201,.055) 1px,transparent 1px),var(--paper);background-size:32px 32px}.hero{padding:44px clamp(24px,5vw,72px) 40px;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(420px,.75fr);gap:clamp(40px,7vw,100px);align-items:end;background:var(--paper-light)}.hero-copy{max-width:760px}.hero h1{max-width:700px;margin:10px 0 14px;font:650 clamp(36px,4vw,58px)/1.02 var(--font-display);letter-spacing:-.045em}.hero-copy>p{max-width:720px;margin:0;color:var(--ink-soft);font-size:clamp(15px,1.3vw,18px);line-height:1.55}.hero-principles{border-top:1px solid var(--line)}.hero-principles div{padding:13px 0;display:grid;grid-template-columns:118px 1fr;gap:16px;border-bottom:1px solid var(--line-soft)}.hero-principles strong{font-size:12px}.hero-principles span{color:var(--ink-soft);font-size:11px;line-height:1.45}.workspace-heading{min-height:46px;padding:14px 20px 10px;display:flex;justify-content:space-between;align-items:center;gap:20px;background:#f4fafff0}.results-heading h2{margin:5px 0 0;font:650 22px/1.1 var(--font-display);letter-spacing:-.025em}.mode-note{max-width:190px;padding:7px 9px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--paper-light);font:10px/1.4 var(--font-mono)}.section-index,.field-label{display:block;color:var(--ink-soft);font:500 9px/1.4 var(--font-mono);letter-spacing:.09em;text-transform:uppercase}.search-section{padding:24px 20px 22px;border-bottom:1px solid var(--line)}.search-section h2{max-width:360px;margin:12px 0 10px;font:650 clamp(28px,2.4vw,38px)/1.02 var(--font-display);letter-spacing:-.04em}.lede{margin:0 0 24px;color:var(--ink-soft);font-size:13px;line-height:1.6}.search-section form{margin-top:20px}.search-section textarea{width:100%;margin-top:8px;padding:15px;resize:vertical;border:2px solid var(--line);border-radius:var(--radius-md);background:#fff;color:var(--ink);font-size:13px;line-height:1.55;box-shadow:0 4px 14px #1a689912}.search-section textarea::placeholder{color:#7890a5}.form-meta{margin:10px 0 14px;display:flex;justify-content:space-between;color:var(--ink-soft);font:10px var(--font-mono)}.primary-action{width:100%;min-height:52px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;border:2px solid var(--forest-dark);border-radius:var(--radius-md);background:var(--forest);color:#fff;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 4px 0 var(--forest-dark);transition:transform .12s ease,box-shadow .12s ease}.primary-action:hover:not(:disabled){transform:translateY(2px);box-shadow:0 2px 0 var(--forest-dark)}.primary-action:disabled{cursor:wait;opacity:.65}.examples{margin-top:24px;display:grid;gap:7px}.examples .field-label{margin-bottom:2px}.examples button{padding:9px 0;display:grid;grid-template-columns:28px 1fr;gap:8px;border:0;border-top:1px solid var(--line-soft);background:transparent;color:var(--ink);text-align:left;cursor:pointer;font-size:11px;line-height:1.4}.examples button span{color:var(--amber);font-family:var(--font-mono)}.process-section{padding:20px 20px 26px}.method-list{margin:14px 0 0;padding:0;list-style:none;display:grid}.method-note{margin:10px 0 0;color:var(--ink-soft);font-size:11px;line-height:1.55}.method-list li{padding:10px 0;display:grid;grid-template-columns:28px 1fr;border-top:1px solid var(--line-soft);font-size:11px}.method-list li span{color:var(--forest);font:11px var(--font-mono)}.process-title{display:flex;justify-content:space-between;font:10px var(--font-mono)}.trace-summary{margin-top:12px;display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden;background:var(--paper-light)}.trace-summary div{min-width:0;padding:8px 6px;border-right:1px solid var(--line-soft)}.trace-summary div:last-child{border-right:0}.trace-summary span{display:block;color:var(--ink-soft);font:7px var(--font-mono);text-transform:uppercase}.trace-summary strong{display:block;margin-top:4px;font:650 15px var(--font-display)}.trace-focus{margin-top:10px;padding:10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-blue)}.trace-focus span{display:block;color:var(--ink-soft);font:8px var(--font-mono);text-transform:uppercase}.trace-focus strong{display:block;margin-top:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.event-feed{max-height:380px;margin-top:12px;overflow-y:auto}.event-row{padding:10px 0;display:grid;grid-template-columns:30px 1fr;gap:8px;border-top:1px solid var(--line-soft)}.event-number{color:var(--amber);font:10px var(--font-mono)}.event-row strong{font:600 10px var(--font-mono);text-transform:uppercase}.event-row p{margin:3px 0 0;color:var(--ink-soft);font-size:11px;line-height:1.45}.event-error p{color:var(--clay)}.event-meta{margin-top:7px;display:flex;flex-wrap:wrap;gap:5px}.event-meta span{padding:3px 5px;border:1px solid var(--line-soft);border-radius:999px;background:#fff;color:var(--ink-soft);font:7px var(--font-mono);text-transform:uppercase}.trace-cursor{padding:10px 0;color:var(--forest);font:10px var(--font-mono);animation:blink 1.2s infinite}@keyframes blink{50%{opacity:.35}}.brief-strip{margin:0 20px;padding:14px 16px 16px;border:1px solid var(--line);border-radius:var(--radius-md);background:#ffffffd1}.brief-strip-heading{display:flex;justify-content:space-between;gap:16px;color:var(--ink-soft);font:9px var(--font-mono)}.brief-grid{margin-top:12px;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px}.brief-item{min-width:0;padding:9px 10px;border-radius:var(--radius-sm);background:var(--surface-blue)}.brief-item span{display:block;color:var(--ink-soft);font:8px var(--font-mono);text-transform:uppercase}.brief-item strong{display:block;margin-top:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;font-weight:600}.agent-progress{margin:12px 20px;display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--line);border-radius:var(--radius-md);overflow:hidden;background:var(--paper-light)}.progress-step{min-width:0;padding:12px;display:grid;grid-template-columns:24px 1fr;gap:8px;border-right:1px solid var(--line-soft)}.progress-step:last-child{border-right:0}.progress-step.active{background:var(--surface-blue)}.progress-number{width:22px;height:22px;display:grid;place-items:center;border:1px solid var(--line);border-radius:50%;color:var(--ink-soft);font:9px var(--font-mono)}.progress-step.complete .progress-number{border-color:var(--forest);background:var(--forest);color:#fff}.progress-step strong{display:block;font-size:10px}.progress-step p{margin:3px 0 0;color:var(--ink-soft);font-size:8px;line-height:1.35}.research-grid{min-height:530px;margin:0 20px 20px;display:grid;grid-template-columns:minmax(0,1.7fr) minmax(190px,.8fr);gap:12px}.evidence-panel,.check-panel{min-width:0;display:flex;flex-direction:column;border:1px solid var(--line);border-radius:var(--radius-md);overflow:hidden;background:#ffffffdb}.panel-heading{min-height:70px;padding:14px 16px;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line-soft)}.panel-heading h2{margin:6px 0 0;font:650 17px/1.15 var(--font-display);letter-spacing:-.025em}.panel-count{font:650 22px var(--font-display)}.candidate-board{flex:1;min-height:450px;padding:14px;display:flex;flex-direction:column;gap:12px;overflow:auto}.candidate-board.empty{display:grid;place-content:center;justify-items:center;text-align:center}.candidate-board.empty strong{margin-top:24px;font:650 18px var(--font-display)}.candidate-board.empty p{max-width:390px;color:var(--ink-soft);font-size:12px;line-height:1.6}.workflow-preview{display:flex;align-items:center;justify-content:center}.workflow-preview>div{width:112px;min-height:94px;padding:12px;display:flex;flex-direction:column;border:1px solid var(--line);border-radius:var(--radius-md);background:var(--paper-light);text-align:left}.workflow-preview span{color:var(--forest);font:8px var(--font-mono)}.workflow-preview strong{margin:9px 0 4px;font-size:12px}.workflow-preview small{color:var(--ink-soft);font-size:8px;line-height:1.4}.workflow-preview i{width:24px;height:1px;background:var(--line)}.decision-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;align-items:stretch}.decision-stat{padding:9px 10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:#fff}.decision-stat span{display:block;color:var(--ink-soft);font:8px var(--font-mono);text-transform:uppercase}.decision-stat strong{display:block;margin-top:4px;font:650 20px var(--font-display)}.stat-best strong{color:var(--forest-dark)}.stat-hidden strong{color:var(--forest)}.stat-review strong{color:var(--amber)}.stat-filtered strong{color:var(--clay)}.decision-summary p{margin:0;padding:9px 11px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:#ffffffbd;color:var(--ink-soft);font-size:9px;line-height:1.45;grid-column:1 / -1}.candidate-lanes{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;align-items:start}.candidate-lane{min-width:0;padding:10px;display:grid;gap:8px;border:1px solid var(--line-soft);border-radius:var(--radius-md);background:#ffffffbd}.lane-heading{min-height:46px;display:flex;justify-content:space-between;gap:10px;align-items:flex-start;border-bottom:1px solid var(--line-soft);padding-bottom:9px}.lane-heading strong{display:block;font-size:11px}.lane-heading span{display:block;margin-top:3px;color:var(--ink-soft);font-size:8px;line-height:1.35}.lane-heading b{font:650 18px var(--font-display)}.lane-best .lane-heading b{color:var(--forest-dark)}.lane-hidden .lane-heading b{color:var(--forest)}.lane-review .lane-heading b{color:var(--amber)}.lane-hidden{background:linear-gradient(180deg,#e6f4fee6,#ffffffc2)}.candidate-card{width:100%;padding:10px;border:1px solid transparent;border-radius:var(--radius-sm);background:#fff;color:var(--ink);text-align:left;cursor:pointer;box-shadow:0 1px #12689d14}.candidate-card:hover,.candidate-card.active{border-color:var(--line);background:#fff}.candidate-card.active{border-color:var(--forest);box-shadow:0 2px 10px #12689d1f}.candidate-card-top{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:start}.candidate-card-top strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px}.candidate-card-top span{font:650 16px var(--font-display)}.candidate-badges{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px}.candidate-badges span{padding:3px 6px;border:1px solid rgba(36,137,201,.35);border-radius:999px;background:var(--surface-blue);color:var(--forest-dark);font:7px var(--font-mono);text-transform:uppercase}.candidate-card p{min-height:34px;margin:7px 0;color:var(--ink-soft);font-size:9px;line-height:1.42}.candidate-card small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink-soft);font-size:8px}.check-dots{margin-top:9px;display:flex;flex-wrap:wrap;gap:4px}.check-dot{padding:3px 5px;border-radius:999px;border:1px solid var(--line-soft);color:var(--ink-soft);background:#fff;font:8px var(--font-mono)}.check-dot.check-pass{border-color:#2489c959;color:var(--forest-dark);background:var(--surface-blue)}.check-dot.check-review,.check-dot.check-unknown{border-color:#c47a1859;color:#8c560d;background:#fff8eb}.check-dot.check-fail{border-color:#bd4b5c59;color:var(--clay);background:#fff1f3}.candidate-empty{padding:14px 10px;color:var(--ink-soft);font-size:9px;border:1px dashed var(--line-soft);border-radius:var(--radius-sm)}.check-summary{padding:4px 14px 8px}.check-summary-row{padding:12px 0;display:grid;grid-template-columns:1fr auto;gap:10px;border-bottom:1px solid var(--line-soft)}.check-summary-row strong{font-size:10px}.check-summary-row p{margin:4px 0 0;color:var(--ink-soft);font-size:9px;line-height:1.4}.check-summary-row>span{align-self:center;color:var(--ink-soft);font:8px var(--font-mono)}.check-summary-row>span.has-pass{color:var(--forest-dark)}.check-summary-row>span.has-fail{color:var(--clay)}.honesty-note{margin:auto 14px 14px;padding:12px;border-radius:var(--radius-sm);background:var(--surface-blue)}.honesty-note strong{font-size:10px}.honesty-note p{margin:5px 0 0;color:var(--ink-soft);font-size:9px;line-height:1.45}.detail-card{padding:22px 20px;border-bottom:1px solid var(--line)}.detail-kicker{display:flex;justify-content:space-between;font:10px var(--font-mono);text-transform:uppercase}.detail-kicker strong{font-size:16px}.detail-card h2{margin:14px 0 2px;overflow-wrap:anywhere;font:650 23px/1.08 var(--font-display)}.dataset-id{color:var(--forest);font:10px var(--font-mono);overflow-wrap:anywhere}.detail-description{max-height:110px;margin:17px 0;overflow-y:auto;color:var(--ink-soft);font-size:11px;line-height:1.55}.discovery-note{margin:-6px 0 16px;padding:10px 11px;border:1px solid rgba(36,137,201,.35);border-radius:var(--radius-sm);background:var(--surface-blue);color:var(--forest-dark);font-size:10px;line-height:1.45}.score-grid{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden}.score-grid div{padding:8px 5px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);text-align:center}.score-grid div:nth-child(4n){border-right:0}.score-grid span{display:block;color:var(--ink-soft);font:8px var(--font-mono);text-transform:uppercase}.score-grid strong{display:block;margin-top:3px;font:600 17px var(--font-display)}.detail-block{margin-top:18px}.detail-block .field-label{margin-bottom:8px}.check-grid{border-top:1px solid var(--line-soft)}.check-row{padding:7px 0;display:flex;justify-content:space-between;border-bottom:1px solid var(--line-soft);font-size:10px;text-transform:capitalize}.check-row strong{font:9px var(--font-mono);text-transform:uppercase}.check-pass strong{color:var(--forest)}.check-fail strong{color:var(--clay)}.check-review strong,.check-unknown strong{color:#9a5d0c}.evidence-list{margin:0;padding-left:17px;color:var(--ink-soft);font-size:10px;line-height:1.55}.tag-list{display:flex;flex-wrap:wrap;gap:5px}.tag-list span{padding:4px 6px;border:1px solid var(--line);border-radius:999px;background:var(--surface-blue);font:8px var(--font-mono)}.sample-test-list{display:grid;gap:6px}.sample-test{padding:8px 9px;display:grid;grid-template-columns:1fr auto;gap:4px 8px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:#fff}.sample-test strong{font-size:10px}.sample-test span{font:8px var(--font-mono);text-transform:uppercase}.sample-test p{grid-column:1 / -1;margin:0;color:var(--ink-soft);font-size:9px;line-height:1.4}.sample-pass span{color:var(--forest-dark)}.sample-review span,.sample-unknown span{color:#9a5d0c}.sample-fail span{color:var(--clay)}.loader-snippet{margin:0;padding:10px;overflow-x:auto;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:#f7fbff;color:var(--ink);font:9px/1.5 var(--font-mono)}.verdict{margin-top:18px;padding:13px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-blue)}.verdict strong{font-size:11px;line-height:1.45}.verdict p{margin:5px 0 0;color:var(--ink-soft);font-size:10px;line-height:1.5}.results-section{padding:20px}.results-heading{display:flex;justify-content:space-between;align-items:flex-end}.results-heading>span{font:600 24px var(--font-display)}.empty-results{margin-top:20px;padding:18px;border:1px dashed var(--line);border-radius:var(--radius-md);color:var(--ink-soft)}.empty-results strong{color:var(--ink);font:600 16px var(--font-display)}.empty-results p{margin:7px 0 0;font-size:10px;line-height:1.5}.result-list{margin-top:18px;border-top:2px solid var(--line)}.result-row{width:100%;min-height:66px;padding:10px 8px;display:grid;grid-template-columns:28px minmax(0,1fr) 40px;gap:8px;align-items:center;border:0;border-bottom:1px solid var(--line-soft);border-radius:var(--radius-sm);background:transparent;color:var(--ink);cursor:pointer;text-align:left}.result-row:hover{background:var(--surface-blue)}.result-rank{color:var(--ink-soft);font:10px var(--font-mono)}.result-main{min-width:0}.result-main strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px}.result-main span{display:block;margin-top:5px;font:8px var(--font-mono);text-transform:uppercase}.status-recommended .result-main span{color:var(--forest)}.status-conditional .result-main span{color:#9a5d0c}.status-rejected .result-main span{color:var(--clay)}.result-score{font:600 22px var(--font-display);text-align:right}.mobile-tabs{display:none}@media(max-width:1180px){.workspace{grid-template-columns:280px minmax(480px,1fr) 300px}.topbar-meta>span:first-child{display:none}}@media(max-width:1400px){.research-grid{grid-template-columns:1fr}.check-panel{min-height:auto}.check-summary{display:grid;grid-template-columns:repeat(2,1fr);gap:0 16px}.honesty-note{margin-top:10px}}@media(max-width:900px){body{overflow:auto}.topbar{min-height:62px;padding:0 16px}.brand{font-size:19px}.topbar-meta{gap:0}.hero{padding:32px 20px;grid-template-columns:1fr;gap:28px}.hero h1{font-size:38px}.workspace{display:block}.mobile-tabs{position:sticky;top:0;z-index:10;display:grid;grid-template-columns:repeat(3,1fr);border-bottom:2px solid var(--line);background:var(--paper-light)}.mobile-tabs button{padding:13px;border:0;border-right:1px solid var(--line);background:transparent;font:10px var(--font-mono);text-transform:uppercase}.mobile-tabs button.active{background:var(--forest);color:#fff}.rail,.map-stage{display:none;max-height:none;border:0}.rail.mobile-active,.map-stage.mobile-active{display:block}.map-stage.mobile-active{display:flex;min-height:calc(100vh - 108px)}.candidate-board{min-height:480px}.candidate-lanes{grid-template-columns:1fr}.search-section h2{font-size:34px}.brief-grid,.agent-progress{grid-template-columns:repeat(2,1fr)}.progress-step{border-bottom:1px solid var(--line-soft)}.progress-step:nth-child(2n){border-right:0}.progress-step:last-child{border-bottom:0}}@media(max-width:520px){.topbar-meta{display:none}.search-section,.process-section,.detail-card,.results-section{padding-left:17px;padding-right:17px}.hero{padding:26px 17px}.hero h1{font-size:33px}.hero-principles div{grid-template-columns:100px 1fr}.search-section h2{font-size:31px}.workspace-heading{padding-left:17px;padding-right:17px}.mode-note{display:none}.brief-strip,.agent-progress,.research-grid{margin-left:12px;margin-right:12px}.brief-strip-heading>span:last-child{display:none}.brief-grid{grid-template-columns:1fr 1fr}.brief-item:last-child{grid-column:1 / -1}.agent-progress{grid-template-columns:1fr}.progress-step{border-right:0;border-bottom:1px solid var(--line-soft)}.progress-step:last-child{border-bottom:0}.research-grid{min-height:0}.workflow-preview{flex-direction:column}.workflow-preview i{width:1px;height:14px}.candidate-board{min-height:500px}.check-summary{grid-template-columns:1fr}.score-grid{grid-template-columns:repeat(2,1fr)}.score-grid div:nth-child(2n){border-right:0}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
|
frontend/dist/assets/index-D64Ptpvl.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
| 6 |
<meta name="theme-color" content="#f4faff" />
|
| 7 |
<meta name="description" content="Evidence-led agentic dataset discovery for Hugging Face." />
|
| 8 |
<title>HF Agentic Search</title>
|
| 9 |
-
<script type="module" crossorigin src="/assets/index-
|
| 10 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
<div id="root"></div>
|
|
|
|
| 6 |
<meta name="theme-color" content="#f4faff" />
|
| 7 |
<meta name="description" content="Evidence-led agentic dataset discovery for Hugging Face." />
|
| 8 |
<title>HF Agentic Search</title>
|
| 9 |
+
<script type="module" crossorigin src="/assets/index-D64Ptpvl.js"></script>
|
| 10 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BsUEW0Da.css">
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
<div id="root"></div>
|
frontend/src/components/CandidateBoard.jsx
CHANGED
|
@@ -3,14 +3,22 @@ import { useGame } from '../GameProvider.jsx';
|
|
| 3 |
|
| 4 |
const LANES = [
|
| 5 |
{
|
| 6 |
-
|
| 7 |
-
title: '
|
| 8 |
description: 'Ready for a first experiment',
|
|
|
|
| 9 |
},
|
| 10 |
{
|
| 11 |
-
|
| 12 |
-
title: '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
description: 'Useful, but one or more checks need attention',
|
|
|
|
| 14 |
},
|
| 15 |
];
|
| 16 |
|
|
@@ -45,7 +53,7 @@ export default function CandidateBoard() {
|
|
| 45 |
const rejectedCount = datasets.length - visibleDatasets.length;
|
| 46 |
const counts = LANES.map((lane) => ({
|
| 47 |
...lane,
|
| 48 |
-
count: visibleDatasets.filter(
|
| 49 |
}));
|
| 50 |
|
| 51 |
if (!datasets.length) {
|
|
@@ -68,7 +76,7 @@ export default function CandidateBoard() {
|
|
| 68 |
<div className="candidate-board">
|
| 69 |
<div className="decision-summary">
|
| 70 |
{counts.map((lane) => (
|
| 71 |
-
<div className={`decision-stat stat-${lane.
|
| 72 |
<span>{lane.title}</span>
|
| 73 |
<strong>{lane.count}</strong>
|
| 74 |
</div>
|
|
@@ -82,9 +90,9 @@ export default function CandidateBoard() {
|
|
| 82 |
|
| 83 |
<div className="candidate-lanes">
|
| 84 |
{counts.map((lane) => {
|
| 85 |
-
const laneDatasets = visibleDatasets.filter(
|
| 86 |
return (
|
| 87 |
-
<section className={`candidate-lane lane-${lane.
|
| 88 |
<div className="lane-heading">
|
| 89 |
<div>
|
| 90 |
<strong>{lane.title}</strong>
|
|
@@ -104,8 +112,16 @@ export default function CandidateBoard() {
|
|
| 104 |
<strong title={dataset.id}>{shortName(dataset.id)}</strong>
|
| 105 |
<span>{dataset.score}</span>
|
| 106 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
<p>{mainGap(dataset)}</p>
|
| 108 |
<small>{compactEvidence(dataset)}</small>
|
|
|
|
| 109 |
<div className="check-dots" aria-label="Evidence checks">
|
| 110 |
{CHECKS.map(([key, label]) => {
|
| 111 |
const value = dataset.checks?.[key] || 'unknown';
|
|
|
|
| 3 |
|
| 4 |
const LANES = [
|
| 5 |
{
|
| 6 |
+
key: 'best',
|
| 7 |
+
title: 'Best fits',
|
| 8 |
description: 'Ready for a first experiment',
|
| 9 |
+
filter: (dataset) => dataset.status === 'recommended' && !dataset.badges?.includes('hidden_gem'),
|
| 10 |
},
|
| 11 |
{
|
| 12 |
+
key: 'hidden',
|
| 13 |
+
title: 'Hidden gems',
|
| 14 |
+
description: 'Lower adoption, strong evidence fit',
|
| 15 |
+
filter: (dataset) => dataset.badges?.includes('hidden_gem'),
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
key: 'review',
|
| 19 |
+
title: 'Needs review',
|
| 20 |
description: 'Useful, but one or more checks need attention',
|
| 21 |
+
filter: (dataset) => dataset.status === 'conditional' && !dataset.badges?.includes('hidden_gem'),
|
| 22 |
},
|
| 23 |
];
|
| 24 |
|
|
|
|
| 53 |
const rejectedCount = datasets.length - visibleDatasets.length;
|
| 54 |
const counts = LANES.map((lane) => ({
|
| 55 |
...lane,
|
| 56 |
+
count: visibleDatasets.filter(lane.filter).length,
|
| 57 |
}));
|
| 58 |
|
| 59 |
if (!datasets.length) {
|
|
|
|
| 76 |
<div className="candidate-board">
|
| 77 |
<div className="decision-summary">
|
| 78 |
{counts.map((lane) => (
|
| 79 |
+
<div className={`decision-stat stat-${lane.key}`} key={lane.key}>
|
| 80 |
<span>{lane.title}</span>
|
| 81 |
<strong>{lane.count}</strong>
|
| 82 |
</div>
|
|
|
|
| 90 |
|
| 91 |
<div className="candidate-lanes">
|
| 92 |
{counts.map((lane) => {
|
| 93 |
+
const laneDatasets = visibleDatasets.filter(lane.filter);
|
| 94 |
return (
|
| 95 |
+
<section className={`candidate-lane lane-${lane.key}`} key={lane.key}>
|
| 96 |
<div className="lane-heading">
|
| 97 |
<div>
|
| 98 |
<strong>{lane.title}</strong>
|
|
|
|
| 112 |
<strong title={dataset.id}>{shortName(dataset.id)}</strong>
|
| 113 |
<span>{dataset.score}</span>
|
| 114 |
</div>
|
| 115 |
+
{dataset.badges?.length ? (
|
| 116 |
+
<div className="candidate-badges">
|
| 117 |
+
{dataset.badges.map((badge) => (
|
| 118 |
+
<span key={badge}>{badge.replaceAll('_', ' ')}</span>
|
| 119 |
+
))}
|
| 120 |
+
</div>
|
| 121 |
+
) : null}
|
| 122 |
<p>{mainGap(dataset)}</p>
|
| 123 |
<small>{compactEvidence(dataset)}</small>
|
| 124 |
+
{dataset.sample_test_summary ? <small>{dataset.sample_test_summary}</small> : null}
|
| 125 |
<div className="check-dots" aria-label="Evidence checks">
|
| 126 |
{CHECKS.map(([key, label]) => {
|
| 127 |
const value = dataset.checks?.[key] || 'unknown';
|
frontend/src/components/ResearchWorkspace.jsx
CHANGED
|
@@ -6,6 +6,7 @@ const STEPS = [
|
|
| 6 |
{ type: 'plan', label: 'Understand', detail: 'Turn the brief into explicit requirements' },
|
| 7 |
{ type: 'search', label: 'Search', detail: 'Explore several angles across the Hub' },
|
| 8 |
{ type: 'inspect', label: 'Inspect', detail: 'Read cards, schemas and sample rows' },
|
|
|
|
| 9 |
{ type: 'ranking', label: 'Rank', detail: 'Compare evidence and explain the result' },
|
| 10 |
];
|
| 11 |
|
|
|
|
| 6 |
{ type: 'plan', label: 'Understand', detail: 'Turn the brief into explicit requirements' },
|
| 7 |
{ type: 'search', label: 'Search', detail: 'Explore several angles across the Hub' },
|
| 8 |
{ type: 'inspect', label: 'Inspect', detail: 'Read cards, schemas and sample rows' },
|
| 9 |
+
{ type: 'reflect', label: 'Reflect', detail: 'Revise the search from evidence gaps' },
|
| 10 |
{ type: 'ranking', label: 'Rank', detail: 'Compare evidence and explain the result' },
|
| 11 |
];
|
| 12 |
|
frontend/src/hud/AgentLog.jsx
CHANGED
|
@@ -6,6 +6,7 @@ const LABELS = {
|
|
| 6 |
plan: 'Plan',
|
| 7 |
search: 'Search',
|
| 8 |
inspect: 'Inspect',
|
|
|
|
| 9 |
ranking: 'Rank',
|
| 10 |
complete: 'Done',
|
| 11 |
error: 'Error',
|
|
@@ -50,6 +51,14 @@ function EventMeta({ event }) {
|
|
| 50 |
</div>
|
| 51 |
);
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
return null;
|
| 54 |
}
|
| 55 |
|
|
|
|
| 6 |
plan: 'Plan',
|
| 7 |
search: 'Search',
|
| 8 |
inspect: 'Inspect',
|
| 9 |
+
reflect: 'Reflect',
|
| 10 |
ranking: 'Rank',
|
| 11 |
complete: 'Done',
|
| 12 |
error: 'Error',
|
|
|
|
| 51 |
</div>
|
| 52 |
);
|
| 53 |
}
|
| 54 |
+
if (event.type === 'reflect' && event.next_queries?.length) {
|
| 55 |
+
return (
|
| 56 |
+
<div className="event-meta">
|
| 57 |
+
<span>{event.strategy}</span>
|
| 58 |
+
<span>{event.next_queries.length} next angles</span>
|
| 59 |
+
</div>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
return null;
|
| 63 |
}
|
| 64 |
|
frontend/src/hud/DetailCard.jsx
CHANGED
|
@@ -10,6 +10,16 @@ function Check({ label, value }) {
|
|
| 10 |
);
|
| 11 |
}
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
export default function DetailCard() {
|
| 14 |
const { selected } = useGame();
|
| 15 |
if (!selected) return null;
|
|
@@ -27,6 +37,7 @@ export default function DetailCard() {
|
|
| 27 |
<p className="detail-description">
|
| 28 |
{selected.description || 'No dataset-card description was available.'}
|
| 29 |
</p>
|
|
|
|
| 30 |
|
| 31 |
<div className="score-grid">
|
| 32 |
{Object.entries(selected.score_breakdown || {}).map(([key, value]) => (
|
|
@@ -62,6 +73,22 @@ export default function DetailCard() {
|
|
| 62 |
</div>
|
| 63 |
) : null}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
<div className="verdict">
|
| 66 |
<strong>{selected.recommendation}</strong>
|
| 67 |
<p>{selected.weakness}</p>
|
|
|
|
| 10 |
);
|
| 11 |
}
|
| 12 |
|
| 13 |
+
function SampleTest({ test }) {
|
| 14 |
+
return (
|
| 15 |
+
<div className={`sample-test sample-${test.status}`}>
|
| 16 |
+
<strong>{test.name}</strong>
|
| 17 |
+
<span>{test.status}</span>
|
| 18 |
+
<p>{test.detail}</p>
|
| 19 |
+
</div>
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
export default function DetailCard() {
|
| 24 |
const { selected } = useGame();
|
| 25 |
if (!selected) return null;
|
|
|
|
| 37 |
<p className="detail-description">
|
| 38 |
{selected.description || 'No dataset-card description was available.'}
|
| 39 |
</p>
|
| 40 |
+
{selected.discovery_note ? <p className="discovery-note">{selected.discovery_note}</p> : null}
|
| 41 |
|
| 42 |
<div className="score-grid">
|
| 43 |
{Object.entries(selected.score_breakdown || {}).map(([key, value]) => (
|
|
|
|
| 73 |
</div>
|
| 74 |
) : null}
|
| 75 |
|
| 76 |
+
{selected.sample_tests?.length ? (
|
| 77 |
+
<div className="detail-block">
|
| 78 |
+
<span className="field-label">Sample tests</span>
|
| 79 |
+
<div className="sample-test-list">
|
| 80 |
+
{selected.sample_tests.map((test) => <SampleTest key={test.name} test={test} />)}
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
) : null}
|
| 84 |
+
|
| 85 |
+
{selected.loader_snippet ? (
|
| 86 |
+
<div className="detail-block">
|
| 87 |
+
<span className="field-label">Starter loader</span>
|
| 88 |
+
<pre className="loader-snippet">{selected.loader_snippet}</pre>
|
| 89 |
+
</div>
|
| 90 |
+
) : null}
|
| 91 |
+
|
| 92 |
<div className="verdict">
|
| 93 |
<strong>{selected.recommendation}</strong>
|
| 94 |
<p>{selected.weakness}</p>
|
frontend/src/styles.css
CHANGED
|
@@ -340,7 +340,7 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 340 |
.agent-progress {
|
| 341 |
margin: 12px 20px;
|
| 342 |
display: grid;
|
| 343 |
-
grid-template-columns: repeat(
|
| 344 |
border: 1px solid var(--line);
|
| 345 |
border-radius: var(--radius-md);
|
| 346 |
overflow: hidden;
|
|
@@ -454,8 +454,9 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 454 |
margin-top: 4px;
|
| 455 |
font: 650 20px var(--font-display);
|
| 456 |
}
|
| 457 |
-
.stat-
|
| 458 |
-
.stat-
|
|
|
|
| 459 |
.stat-filtered strong { color: var(--clay); }
|
| 460 |
.decision-summary p {
|
| 461 |
margin: 0;
|
|
@@ -496,8 +497,12 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 496 |
.lane-heading strong { display: block; font-size: 11px; }
|
| 497 |
.lane-heading span { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 8px; line-height: 1.35; }
|
| 498 |
.lane-heading b { font: 650 18px var(--font-display); }
|
| 499 |
-
.lane-
|
| 500 |
-
.lane-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
.candidate-card {
|
| 502 |
width: 100%;
|
| 503 |
padding: 10px;
|
|
@@ -531,6 +536,21 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 531 |
font-size: 10px;
|
| 532 |
}
|
| 533 |
.candidate-card-top span { font: 650 16px var(--font-display); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
.candidate-card p {
|
| 535 |
min-height: 34px;
|
| 536 |
margin: 7px 0;
|
|
@@ -611,6 +631,16 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 611 |
font-size: 11px;
|
| 612 |
line-height: 1.55;
|
| 613 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
.score-grid { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
|
| 615 |
.score-grid div { padding: 8px 5px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); text-align: center; }
|
| 616 |
.score-grid div:nth-child(4n) { border-right: 0; }
|
|
@@ -634,6 +664,38 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 634 |
.evidence-list { margin: 0; padding-left: 17px; color: var(--ink-soft); font-size: 10px; line-height: 1.55; }
|
| 635 |
.tag-list { display: flex; flex-wrap: wrap; gap: 5px; }
|
| 636 |
.tag-list span { padding: 4px 6px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-blue); font: 8px var(--font-mono); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
.verdict { margin-top: 18px; padding: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-blue); }
|
| 638 |
.verdict strong { font-size: 11px; line-height: 1.45; }
|
| 639 |
.verdict p { margin: 5px 0 0; color: var(--ink-soft); font-size: 10px; line-height: 1.5; }
|
|
@@ -728,8 +790,9 @@ button:focus-visible, a:focus-visible, textarea:focus-visible {
|
|
| 728 |
.search-section h2 { font-size: 34px; }
|
| 729 |
.brief-grid { grid-template-columns: repeat(2, 1fr); }
|
| 730 |
.agent-progress { grid-template-columns: repeat(2, 1fr); }
|
| 731 |
-
.progress-step
|
| 732 |
-
.progress-step:nth-child(
|
|
|
|
| 733 |
}
|
| 734 |
|
| 735 |
@media (max-width: 520px) {
|
|
|
|
| 340 |
.agent-progress {
|
| 341 |
margin: 12px 20px;
|
| 342 |
display: grid;
|
| 343 |
+
grid-template-columns: repeat(5, 1fr);
|
| 344 |
border: 1px solid var(--line);
|
| 345 |
border-radius: var(--radius-md);
|
| 346 |
overflow: hidden;
|
|
|
|
| 454 |
margin-top: 4px;
|
| 455 |
font: 650 20px var(--font-display);
|
| 456 |
}
|
| 457 |
+
.stat-best strong { color: var(--forest-dark); }
|
| 458 |
+
.stat-hidden strong { color: var(--forest); }
|
| 459 |
+
.stat-review strong { color: var(--amber); }
|
| 460 |
.stat-filtered strong { color: var(--clay); }
|
| 461 |
.decision-summary p {
|
| 462 |
margin: 0;
|
|
|
|
| 497 |
.lane-heading strong { display: block; font-size: 11px; }
|
| 498 |
.lane-heading span { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 8px; line-height: 1.35; }
|
| 499 |
.lane-heading b { font: 650 18px var(--font-display); }
|
| 500 |
+
.lane-best .lane-heading b { color: var(--forest-dark); }
|
| 501 |
+
.lane-hidden .lane-heading b { color: var(--forest); }
|
| 502 |
+
.lane-review .lane-heading b { color: var(--amber); }
|
| 503 |
+
.lane-hidden {
|
| 504 |
+
background: linear-gradient(180deg, rgba(230, 244, 254, 0.9), rgba(255, 255, 255, 0.76));
|
| 505 |
+
}
|
| 506 |
.candidate-card {
|
| 507 |
width: 100%;
|
| 508 |
padding: 10px;
|
|
|
|
| 536 |
font-size: 10px;
|
| 537 |
}
|
| 538 |
.candidate-card-top span { font: 650 16px var(--font-display); }
|
| 539 |
+
.candidate-badges {
|
| 540 |
+
margin-top: 6px;
|
| 541 |
+
display: flex;
|
| 542 |
+
flex-wrap: wrap;
|
| 543 |
+
gap: 4px;
|
| 544 |
+
}
|
| 545 |
+
.candidate-badges span {
|
| 546 |
+
padding: 3px 6px;
|
| 547 |
+
border: 1px solid rgba(36, 137, 201, 0.35);
|
| 548 |
+
border-radius: 999px;
|
| 549 |
+
background: var(--surface-blue);
|
| 550 |
+
color: var(--forest-dark);
|
| 551 |
+
font: 7px var(--font-mono);
|
| 552 |
+
text-transform: uppercase;
|
| 553 |
+
}
|
| 554 |
.candidate-card p {
|
| 555 |
min-height: 34px;
|
| 556 |
margin: 7px 0;
|
|
|
|
| 631 |
font-size: 11px;
|
| 632 |
line-height: 1.55;
|
| 633 |
}
|
| 634 |
+
.discovery-note {
|
| 635 |
+
margin: -6px 0 16px;
|
| 636 |
+
padding: 10px 11px;
|
| 637 |
+
border: 1px solid rgba(36, 137, 201, 0.35);
|
| 638 |
+
border-radius: var(--radius-sm);
|
| 639 |
+
background: var(--surface-blue);
|
| 640 |
+
color: var(--forest-dark);
|
| 641 |
+
font-size: 10px;
|
| 642 |
+
line-height: 1.45;
|
| 643 |
+
}
|
| 644 |
.score-grid { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
|
| 645 |
.score-grid div { padding: 8px 5px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); text-align: center; }
|
| 646 |
.score-grid div:nth-child(4n) { border-right: 0; }
|
|
|
|
| 664 |
.evidence-list { margin: 0; padding-left: 17px; color: var(--ink-soft); font-size: 10px; line-height: 1.55; }
|
| 665 |
.tag-list { display: flex; flex-wrap: wrap; gap: 5px; }
|
| 666 |
.tag-list span { padding: 4px 6px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface-blue); font: 8px var(--font-mono); }
|
| 667 |
+
.sample-test-list { display: grid; gap: 6px; }
|
| 668 |
+
.sample-test {
|
| 669 |
+
padding: 8px 9px;
|
| 670 |
+
display: grid;
|
| 671 |
+
grid-template-columns: 1fr auto;
|
| 672 |
+
gap: 4px 8px;
|
| 673 |
+
border: 1px solid var(--line-soft);
|
| 674 |
+
border-radius: var(--radius-sm);
|
| 675 |
+
background: #fff;
|
| 676 |
+
}
|
| 677 |
+
.sample-test strong { font-size: 10px; }
|
| 678 |
+
.sample-test span { font: 8px var(--font-mono); text-transform: uppercase; }
|
| 679 |
+
.sample-test p {
|
| 680 |
+
grid-column: 1 / -1;
|
| 681 |
+
margin: 0;
|
| 682 |
+
color: var(--ink-soft);
|
| 683 |
+
font-size: 9px;
|
| 684 |
+
line-height: 1.4;
|
| 685 |
+
}
|
| 686 |
+
.sample-pass span { color: var(--forest-dark); }
|
| 687 |
+
.sample-review span, .sample-unknown span { color: #9a5d0c; }
|
| 688 |
+
.sample-fail span { color: var(--clay); }
|
| 689 |
+
.loader-snippet {
|
| 690 |
+
margin: 0;
|
| 691 |
+
padding: 10px;
|
| 692 |
+
overflow-x: auto;
|
| 693 |
+
border: 1px solid var(--line-soft);
|
| 694 |
+
border-radius: var(--radius-sm);
|
| 695 |
+
background: #f7fbff;
|
| 696 |
+
color: var(--ink);
|
| 697 |
+
font: 9px/1.5 var(--font-mono);
|
| 698 |
+
}
|
| 699 |
.verdict { margin-top: 18px; padding: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-blue); }
|
| 700 |
.verdict strong { font-size: 11px; line-height: 1.45; }
|
| 701 |
.verdict p { margin: 5px 0 0; color: var(--ink-soft); font-size: 10px; line-height: 1.5; }
|
|
|
|
| 790 |
.search-section h2 { font-size: 34px; }
|
| 791 |
.brief-grid { grid-template-columns: repeat(2, 1fr); }
|
| 792 |
.agent-progress { grid-template-columns: repeat(2, 1fr); }
|
| 793 |
+
.progress-step { border-bottom: 1px solid var(--line-soft); }
|
| 794 |
+
.progress-step:nth-child(2n) { border-right: 0; }
|
| 795 |
+
.progress-step:last-child { border-bottom: 0; }
|
| 796 |
}
|
| 797 |
|
| 798 |
@media (max-width: 520px) {
|
tests/test_agent.py
CHANGED
|
@@ -190,6 +190,29 @@ class AgentTests(unittest.TestCase):
|
|
| 190 |
self.assertEqual(tiny["status"], "conditional")
|
| 191 |
self.assertGreater(training_sized["score"], tiny["score"])
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
def test_queries_are_short_and_hub_friendly(self):
|
| 194 |
profile, _ = parse_task(
|
| 195 |
"English customer support intent data with labels for a compact classifier",
|
|
@@ -364,9 +387,48 @@ class AgentTests(unittest.TestCase):
|
|
| 364 |
self.assertEqual(event_types[1], "plan")
|
| 365 |
self.assertIn("search", event_types)
|
| 366 |
self.assertIn("inspect", event_types)
|
|
|
|
| 367 |
self.assertEqual(event_types[-2:], ["ranking", "complete"])
|
| 368 |
self.assertEqual(events[-1]["result"]["top_pick"], candidate["id"])
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
@patch("backend.agent._llm", return_value=None)
|
| 371 |
def test_search_angles_are_diversified_before_inspection(self, _mock_llm):
|
| 372 |
popular = [
|
|
|
|
| 190 |
self.assertEqual(tiny["status"], "conditional")
|
| 191 |
self.assertGreater(training_sized["score"], tiny["score"])
|
| 192 |
|
| 193 |
+
def test_hidden_gem_sample_tests_and_loader_are_exposed(self):
|
| 194 |
+
profile, _ = parse_task(
|
| 195 |
+
"Climate science question-answer pairs for retrieval",
|
| 196 |
+
use_llm=False,
|
| 197 |
+
)
|
| 198 |
+
scored = score_dataset(
|
| 199 |
+
profile,
|
| 200 |
+
dataset(
|
| 201 |
+
id="small-lab/climate-qa-gem",
|
| 202 |
+
description="Climate science question-answer pairs.",
|
| 203 |
+
downloads=90,
|
| 204 |
+
likes=3,
|
| 205 |
+
features=["question", "answer"],
|
| 206 |
+
sample_rows=[{"question": "How does climate affect rainfall?", "answer": "It changes rainfall patterns."}],
|
| 207 |
+
configs=["default"],
|
| 208 |
+
splits=["train"],
|
| 209 |
+
),
|
| 210 |
+
)
|
| 211 |
+
self.assertIn("hidden_gem", scored["badges"])
|
| 212 |
+
self.assertTrue(scored["sample_tests"])
|
| 213 |
+
self.assertIn("sample tests passed", scored["sample_test_summary"])
|
| 214 |
+
self.assertIn('load_dataset("small-lab/climate-qa-gem")', scored["loader_snippet"])
|
| 215 |
+
|
| 216 |
def test_queries_are_short_and_hub_friendly(self):
|
| 217 |
profile, _ = parse_task(
|
| 218 |
"English customer support intent data with labels for a compact classifier",
|
|
|
|
| 387 |
self.assertEqual(event_types[1], "plan")
|
| 388 |
self.assertIn("search", event_types)
|
| 389 |
self.assertIn("inspect", event_types)
|
| 390 |
+
self.assertIn("reflect", event_types)
|
| 391 |
self.assertEqual(event_types[-2:], ["ranking", "complete"])
|
| 392 |
self.assertEqual(events[-1]["result"]["top_pick"], candidate["id"])
|
| 393 |
|
| 394 |
+
@patch("backend.agent._llm", return_value=None)
|
| 395 |
+
def test_reflection_second_pass_can_recover_a_schema_gem(self, _mock_llm):
|
| 396 |
+
weak = dataset(
|
| 397 |
+
id="broad/climate-reports",
|
| 398 |
+
description="Climate science reports.",
|
| 399 |
+
features=["text"],
|
| 400 |
+
sample_rows=[{"text": "Climate report"}],
|
| 401 |
+
)
|
| 402 |
+
gem = dataset(
|
| 403 |
+
id="niche/climate-qa-gem",
|
| 404 |
+
description="Climate science question-answer pairs.",
|
| 405 |
+
downloads=12,
|
| 406 |
+
likes=1,
|
| 407 |
+
features=["question", "answer"],
|
| 408 |
+
sample_rows=[{"question": "What is climate sensitivity?", "answer": "A warming estimate."}],
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
def fake_search(query, limit=35):
|
| 412 |
+
return [gem] if "qa dataset" in query else [weak]
|
| 413 |
+
|
| 414 |
+
def fake_inspect(_dataset_id, base):
|
| 415 |
+
return base
|
| 416 |
+
|
| 417 |
+
with (
|
| 418 |
+
patch("backend.agent.search_datasets", side_effect=fake_search),
|
| 419 |
+
patch("backend.agent.inspect_dataset", side_effect=fake_inspect),
|
| 420 |
+
):
|
| 421 |
+
events = list(weave_events("Climate science question-answer pairs for retrieval"))
|
| 422 |
+
|
| 423 |
+
result = events[-1]["result"]
|
| 424 |
+
event_types = [event["type"] for event in events]
|
| 425 |
+
self.assertIn("reflect", event_types)
|
| 426 |
+
self.assertEqual(result["top_pick"], gem["id"])
|
| 427 |
+
self.assertTrue(any(
|
| 428 |
+
dataset["id"] == gem["id"] and "hidden_gem" in dataset.get("badges", [])
|
| 429 |
+
for dataset in result["datasets"]
|
| 430 |
+
))
|
| 431 |
+
|
| 432 |
@patch("backend.agent._llm", return_value=None)
|
| 433 |
def test_search_angles_are_diversified_before_inspection(self, _mock_llm):
|
| 434 |
popular = [
|