Files

239 lines
7.6 KiB
Python
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
#!/usr/bin/env python3
"""Convert the PlantGeneBench 100-question Markdown table to JSONL tasks/gold."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
LAYER_CATEGORY = {
"TU": "task_understanding",
"EP": "execution_planning",
"EC": "evidence_constraint",
"BI": "biological_interpretation",
"BC": "boundary_control",
}
ANALYSIS_TYPE_BY_STAGE = {
"rnaseq_qc": "rnaseq",
"rnaseq_align": "rnaseq",
"rnaseq_quant": "rnaseq",
"rnaseq_sample_qc": "rnaseq",
"rnaseq_de": "rnaseq",
"rnaseq_enrich": "rnaseq",
"rnaseq_wgcna": "rnaseq",
"rnaseq_fgsea": "rnaseq",
"rnaseq_candidate_family": "rnaseq",
}
def split_md_row(line: str) -> list[str]:
return [cell.strip() for cell in line.strip().strip("|").split("|")]
def parse_list_expr(text: str, key: str) -> list[str]:
match = re.search(rf"{re.escape(key)}=\[([^\]]*)\]", text)
if not match:
return []
value = match.group(1)
return [
item.strip().strip("'\"` ")
for item in re.split(r",|,", value)
if item.strip().strip("'\"` ")
]
def parse_target_stages(text: str) -> list[str]:
stages = parse_list_expr(text, "target_stages")
if stages:
return stages
stages = parse_list_expr(text, "target")
if stages:
return stages
# A few rows describe executable stages without literal target_stages.
if "map to " in text:
mapped: list[str] = []
for stage in [
"identify",
"motif",
"domain",
"phylogeny",
"synteny",
"interspecies_synteny",
"promoter",
"pep",
"kaks",
]:
if re.search(rf"\b{stage}\b", text):
mapped.append(stage)
return mapped
return []
def parse_execution_plan(text: str, target_stages: list[str]) -> list[str]:
stages = parse_list_expr(text, "execution_plan")
if stages:
return stages
arrow = re.search(r"([a-zA-Z0-9_]+(?:\s*->\s*[a-zA-Z0-9_]+)+)", text)
if arrow:
return [part.strip() for part in arrow.group(1).split("->")]
return target_stages
def parse_params(text: str) -> dict[str, Any]:
params: dict[str, Any] = {}
patterns = {
"promoter_length": r"promoter_length=([0-9]+)",
"max_motifs": r"max_motifs=([0-9]+)",
"bootstrap": r"bootstrap=([0-9]+)",
"geneFamily": r"family=([A-Za-z0-9\-]+)",
"species": r"species=([A-Za-z_]+)",
}
for key, pattern in patterns.items():
match = re.search(pattern, text)
if match:
value: Any = match.group(1)
if key in {"promoter_length", "max_motifs", "bootstrap"}:
value = int(value)
params[key] = value
if "tree_method=ML" in text:
params["tree_method"] = "ML"
if "analysis_input=DEG_result" in text:
params["analysis_input"] = "DEG_result"
return params
def infer_analysis_type(stages: list[str], category: str) -> str:
if category in {"evidence_constraint", "biological_interpretation", "boundary_control"}:
return category
if any(stage in ANALYSIS_TYPE_BY_STAGE for stage in stages):
return "rnaseq"
return "gene_family"
def forbidden_stages(must_not: str) -> list[str]:
found: list[str] = []
stage_terms = {
"identify": ["identify"],
"phylogeny": ["phylogeny"],
"motif": ["motif"],
"domain": ["domain"],
"structure": ["structure"],
"promoter": ["promoter"],
"synteny": ["synteny"],
"interspecies_synteny": ["interspecies_synteny"],
"karyotype": ["karyotype"],
"combo": ["combo"],
"ppi": ["PPI", "ppi"],
"expression": ["expression"],
"pep": ["pep"],
"rnaseq_qc": ["rnaseq_qc"],
"rnaseq_align": ["rnaseq_align"],
"rnaseq_quant": ["rnaseq_quant"],
"rnaseq_de": ["rnaseq_de"],
"rnaseq_enrich": ["rnaseq_enrich"],
}
action_patterns = [
"不要安排",
"不要运行",
"不要重新跑",
"不要加入",
"不要调用",
"不要转到",
"不要启动",
"不要强制",
"不要直接",
"不要只运行",
"不要创建",
]
for stage, terms in stage_terms.items():
for term in terms:
if any(f"{action}{term}" in must_not or f"{action} {term}" in must_not for action in action_patterns):
found.append(stage)
if "不要安排 expression、motif 或 RNA-seq" in must_not or "不要运行 raw RNA-seq" in must_not:
found.extend(["rnaseq_qc", "rnaseq_align", "rnaseq_quant", "rnaseq_de", "rnaseq_enrich"])
return sorted(set(found))
def convert(md_path: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
tasks: list[dict[str, Any]] = []
gold: list[dict[str, Any]] = []
for line in md_path.read_text(encoding="utf-8").splitlines():
if not line.startswith("| PGB-"):
continue
cells = split_md_row(line)
if len(cells) < 5:
continue
task_id, difficulty, query, expected_text, must_not = cells[:5]
layer_code = task_id.split("-")[1]
category = LAYER_CATEGORY.get(layer_code, "unknown")
target_stages = parse_target_stages(expected_text)
execution_plan = parse_execution_plan(expected_text, target_stages)
params = parse_params(expected_text)
analysis_type = infer_analysis_type(target_stages or execution_plan, category)
task = {
"task_id": task_id,
"category": category,
"difficulty": difficulty,
"source_type": "synthetic_expert_curated",
"tags": [category],
"user_query": query,
"input_context": {
"job_id": "",
"prefix": "",
"intent": {
"analysis_type": analysis_type,
"stages": target_stages,
"params": params,
},
},
"gold_summary": expected_text,
"must_not_do": must_not,
}
tasks.append(task)
gold.append(
{
"task_id": task_id,
"gold": {
"analysis_type": analysis_type,
"expected_stages": target_stages,
"expected_execution_plan": execution_plan,
"required_params": params,
"required_dependencies": {},
"forbidden_stages": forbidden_stages(must_not),
"can_execute": "depends_on_inputs",
"rationale": expected_text,
"must_not_do": must_not,
},
}
)
return tasks, gold
def write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--md", type=Path, required=True)
parser.add_argument("--tasks-out", type=Path, required=True)
parser.add_argument("--gold-out", type=Path, required=True)
args = parser.parse_args()
tasks, gold = convert(args.md)
write_jsonl(args.tasks_out, tasks)
write_jsonl(args.gold_out, gold)
print(json.dumps({"tasks": len(tasks), "gold": len(gold)}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())