Files

289 lines
11 KiB
Python
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
#!/usr/bin/env python3
"""Run PlantGeneBench tasks against a provider.
Providers:
- rule: lightweight deterministic baseline for smoke tests.
- api: call the live bbav5 /api/agent/plan endpoint.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List
DEFAULT_TASKS = Path("benchmarks/plantgenebench/tasks/plantgenebench_core_seed.jsonl")
DEFAULT_GOLD = Path("benchmarks/plantgenebench/gold/plantgenebench_core_seed.gold.jsonl")
def load_jsonl(path: Path) -> List[Dict[str, Any]]:
rows: List[Dict[str, Any]] = []
with path.open("r", encoding="utf-8") as handle:
for lineno, line in enumerate(handle, start=1):
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError as exc:
raise SystemExit(f"{path}:{lineno}: invalid JSON: {exc}") from exc
return rows
def dump_jsonl(path: Path, rows: Iterable[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 normalize_list(value: Any) -> List[str]:
if value is None:
return []
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, tuple):
return [str(item).strip() for item in value if str(item).strip()]
return [item for item in re.split(r"[,;,;\s]+", str(value)) if item]
def flatten_params(plan: Dict[str, Any], intent: Dict[str, Any]) -> Dict[str, Any]:
params: Dict[str, Any] = {}
for source in (intent, plan):
for key in ("prefix", "gene_family", "species", "scopeMode"):
if source.get(key) not in (None, ""):
params[key] = source.get(key)
nested = source.get("params")
if isinstance(nested, dict):
params.update(nested)
for detail in plan.get("stages_detail") or []:
if isinstance(detail, dict) and isinstance(detail.get("params"), dict):
for key, value in detail["params"].items():
params.setdefault(key, value)
if "gene_family" in params and "geneFamily" not in params:
params["geneFamily"] = params["gene_family"]
if "target_species" in params and "targetSpecies" not in params:
params["targetSpecies"] = params["target_species"]
if "reference_species" in params and "referenceSpecies" not in params:
params["referenceSpecies"] = params["reference_species"]
return params
def extract_stages(plan: Dict[str, Any], intent: Dict[str, Any]) -> List[str]:
raw = plan.get("stages")
if not raw:
raw = intent.get("stages")
stages: List[str] = []
for item in raw or []:
if isinstance(item, dict):
stage = item.get("id") or item.get("stage") or item.get("name")
else:
stage = item
if stage:
stages.append(str(stage))
return list(dict.fromkeys(stages))
def extract_dependencies(plan: Dict[str, Any]) -> Dict[str, List[str]]:
deps: Dict[str, List[str]] = {}
for detail in plan.get("stages_detail") or []:
if not isinstance(detail, dict):
continue
stage = detail.get("id") or detail.get("stage") or detail.get("name")
if not stage:
continue
deps[str(stage)] = normalize_list(detail.get("dependencies"))
return deps
def normalize_response(raw: Dict[str, Any]) -> Dict[str, Any]:
plan = raw.get("plan") if isinstance(raw.get("plan"), dict) else {}
intent = raw.get("intent") if isinstance(raw.get("intent"), dict) else {}
prediction = {
"success": bool(raw.get("success", False)),
"mode": str(raw.get("mode") or ""),
"analysis_type": str(plan.get("analysis_type") or intent.get("analysis_type") or ""),
"stages": extract_stages(plan, intent),
"params": flatten_params(plan, intent),
"dependencies": extract_dependencies(plan),
"answer_text": str(raw.get("message") or plan.get("description") or intent.get("description") or ""),
}
if raw.get("error"):
prediction["error"] = str(raw["error"])
return prediction
def infer_rule_prediction(task: Dict[str, Any]) -> Dict[str, Any]:
context = task.get("input_context") or {}
intent = context.get("intent") if isinstance(context.get("intent"), dict) else {}
params = dict(intent.get("params") or {})
if context.get("prefix"):
params.setdefault("prefix", context.get("prefix"))
for src_key, dst_key in [
("gene_family", "geneFamily"),
("target_species", "targetSpecies"),
("reference_species", "referenceSpecies"),
]:
if intent.get(src_key) is not None:
params.setdefault(dst_key, intent.get(src_key))
query = str(task.get("user_query") or "").lower()
stages = normalize_list(intent.get("stages"))
analysis_type = str(intent.get("analysis_type") or "gene_family")
if not intent.get("analysis_type") and any(stage.startswith("rnaseq_") for stage in stages):
analysis_type = "rnaseq"
if not intent.get("analysis_type") and ("论文 methods" in query or "methods" in query):
analysis_type = "method_alignment"
if any(term in query for term in ["编", "随便", "没有 fastq", "没有表达矩阵", "fabricat"]):
if task.get("category") == "safety_boundary":
stages = []
analysis_type = "safety"
dependencies = {
"phylogeny": ["identify"],
"motif": ["identify", "phylogeny"],
"domain": ["identify", "phylogeny"],
"structure": ["identify", "phylogeny"],
"promoter": ["identify", "phylogeny"],
"expression": ["identify", "phylogeny"],
"synteny": ["identify"],
"karyotype": ["identify", "synteny"],
"interspecies_synteny": ["identify"],
"ppi": ["identify", "interspecies_synteny"],
"kaks": ["identify", "interspecies_synteny"],
"combo": ["identify", "phylogeny", "motif", "domain", "structure"],
"pep": ["identify"],
"reference_classification": ["phylogeny"],
"rnaseq_align": ["rnaseq_qc"],
"rnaseq_quant": ["rnaseq_align"],
"rnaseq_sample_qc": ["rnaseq_quant"],
"rnaseq_de": ["rnaseq_sample_qc"],
"rnaseq_enrich": ["rnaseq_de"],
}
return {
"success": True,
"mode": "rule_baseline",
"analysis_type": analysis_type,
"stages": stages,
"params": params,
"dependencies": {stage: dependencies.get(stage, []) for stage in stages},
"answer_text": "rule baseline prediction",
}
def call_plan_api(api_base: str, task: Dict[str, Any], timeout: float) -> Dict[str, Any]:
context = task.get("input_context") or {}
intent = dict(context.get("intent") or {})
intent["benchmarkMode"] = True
params = dict(intent.get("params") or {})
params["benchmarkMode"] = True
intent["params"] = params
payload = {
"message": task.get("user_query", ""),
"session_id": f"plantgenebench_{task.get('task_id')}",
"job_id": context.get("job_id", ""),
"prefix": context.get("prefix", ""),
"history": [],
"intent": intent,
}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
f"{api_base.rstrip('/')}/api/agent/plan",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def build_row(
*,
run_id: str,
provider: str,
task: Dict[str, Any],
raw_response: Dict[str, Any],
) -> Dict[str, Any]:
context = task.get("input_context") or {}
return {
"run_id": run_id,
"task_id": task["task_id"],
"provider": provider,
"created_at": datetime.now(timezone.utc).isoformat(),
"request": {
"message": task.get("user_query", ""),
"job_id": context.get("job_id", ""),
"prefix": context.get("prefix", ""),
"intent": context.get("intent", {}),
},
"raw_response": raw_response,
"prediction": normalize_response(raw_response),
}
def run(args: argparse.Namespace) -> int:
tasks = load_jsonl(args.tasks)
if args.limit:
tasks = tasks[: args.limit]
run_id = args.run_id or f"{args.provider}_{int(time.time())}"
rows: List[Dict[str, Any]] = []
for index, task in enumerate(tasks, start=1):
print(f"[{index}/{len(tasks)}] {task['task_id']}", file=sys.stderr)
if args.provider == "rule":
prediction = infer_rule_prediction(task)
raw_response = {
"success": prediction["success"],
"mode": prediction["mode"],
"plan": {
"analysis_type": prediction["analysis_type"],
"stages": prediction["stages"],
"stages_detail": [
{"id": stage, "dependencies": prediction["dependencies"].get(stage, []), "params": prediction["params"]}
for stage in prediction["stages"]
],
"params": prediction["params"],
"description": prediction["answer_text"],
},
"intent": {"stages": prediction["stages"], "params": prediction["params"]},
}
elif args.provider == "api":
try:
raw_response = call_plan_api(args.api_base, task, args.timeout)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
raw_response = {"success": False, "error": str(exc), "plan": {}, "intent": {}}
else:
raise SystemExit(f"unsupported provider: {args.provider}")
rows.append(build_row(run_id=run_id, provider=args.provider, task=task, raw_response=raw_response))
dump_jsonl(args.out, rows)
print(f"Wrote {len(rows)} predictions to {args.out}", file=sys.stderr)
return 0
def parse_args(argv: List[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--provider", choices=["rule", "api"], default="rule")
parser.add_argument("--api-base", default="http://127.0.0.1:7880")
parser.add_argument("--tasks", type=Path, default=DEFAULT_TASKS)
parser.add_argument("--gold", type=Path, default=DEFAULT_GOLD, help="Reserved for run metadata compatibility.")
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--run-id", default="")
parser.add_argument("--timeout", type=float, default=120.0)
parser.add_argument("--limit", type=int, default=0)
return parser.parse_args(argv)
if __name__ == "__main__":
raise SystemExit(run(parse_args(sys.argv[1:])))