sammoftah commited on
Commit
4d56df9
·
verified ·
1 Parent(s): 7ae8c36

Add reflective agent loop and hidden gem evidence

Browse files
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. Run explicit modality, language, required-field, license, and accessibility checks.
46
- 6. Rank candidates from evidence and connect potentially complementary datasets.
47
- 7. Stream the trace and explain verified strengths, limitations, and rejection reasons.
 
 
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
- for query in queries:
 
 
 
 
 
 
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
- global_ranked = sorted(
768
- collected.values(),
769
- key=lambda dataset: _pre_score(profile, dataset),
770
- reverse=True,
 
 
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": "deep candidate pool",
793
  "found": len(pre_ranked),
794
  "unique": len(collected),
795
- "message": f"Prepared {len(pre_ranked)} diverse candidates for evidence inspection.",
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-DEvIQgb1.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-BpRcJTDM.css">
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
- status: 'recommended',
7
- title: 'Recommended',
8
  description: 'Ready for a first experiment',
 
9
  },
10
  {
11
- status: 'conditional',
12
- title: 'Review gaps',
 
 
 
 
 
 
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((dataset) => dataset.status === lane.status).length,
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.status}`} key={lane.status}>
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((dataset) => dataset.status === lane.status);
86
  return (
87
- <section className={`candidate-lane lane-${lane.status}`} key={lane.status}>
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(4, 1fr);
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-recommended strong { color: var(--forest-dark); }
458
- .stat-conditional strong { color: var(--amber); }
 
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-recommended .lane-heading b { color: var(--forest-dark); }
500
- .lane-conditional .lane-heading b { color: var(--amber); }
 
 
 
 
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:nth-child(2) { border-right: 0; }
732
- .progress-step:nth-child(-n + 2) { border-bottom: 1px solid var(--line-soft); }
 
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 = [