from __future__ import annotations import argparse import fcntl import json import os import random import re import sys import types from pathlib import Path from typing import Any PROJECT_ROOT = Path("/225040511/project/Biomni-ReAct") LAB_BENCH_ROOT = Path("/225040511/project/LAB-Bench") DEFAULT_OUTPUT_ROOT = PROJECT_ROOT / "LAB-bench" DEFAULT_DEV_SIZE = 45 DEFAULT_TEST_SIZE = 315 DEFAULT_SEED = 20260514 ANSWER_RE = re.compile(r"\[ANSWER\]\s*([A-Z])\s*\[/ANSWER\]", re.IGNORECASE) SOLUTION_RE = re.compile(r"\s*(.*?)\s*", re.IGNORECASE | re.DOTALL) LETTER_RE = re.compile(r"\b([A-Z])\b", re.IGNORECASE) sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(LAB_BENCH_ROOT)) def install_labbench_import_stubs() -> None: if "vertexai" not in sys.modules: vertexai = types.ModuleType("vertexai") vertexai.init = lambda *_args, **_kwargs: None sys.modules["vertexai"] = vertexai if "google.auth" not in sys.modules: google = sys.modules.setdefault("google", types.ModuleType("google")) auth = types.ModuleType("google.auth") auth.default = lambda *_args, **_kwargs: (types.SimpleNamespace(refresh=lambda *_a, **_k: None, token=""), None) transport = types.ModuleType("google.auth.transport") requests = types.ModuleType("google.auth.transport.requests") requests.Request = lambda *_args, **_kwargs: None transport.requests = requests auth.transport = transport google.auth = auth sys.modules["google.auth"] = auth sys.modules["google.auth.transport"] = transport sys.modules["google.auth.transport.requests"] = requests if "chembench" not in sys.modules: chembench = types.ModuleType("chembench") sys.modules["chembench"] = chembench constant = types.ModuleType("chembench.constant") constant.COT_PROMPT = "Think step by step." constant.MCQ_REGEX_TEMPLATE_1 = r"\[ANSWER\]\s*([A-Z])\s*\[/ANSWER\]" sys.modules["chembench.constant"] = constant prompter = types.ModuleType("chembench.prompter") prompter.prepare_mcq_answer = lambda text, *_args, **_kwargs: text sys.modules["chembench.prompter"] = prompter utils = types.ModuleType("chembench.utils") utils.create_multiple_choice_regex = lambda letters: r"\b(" + "|".join(letters) + r")\b" utils.post_process_prompts = lambda text: text utils.run_regex = lambda _regex, text, return_first=True: None sys.modules["chembench.utils"] = utils install_labbench_import_stubs() import labbench # noqa: E402 from biomni_react.agent import BiomniReActAgent # noqa: E402 from biomni_react.config import AgentConfig # noqa: E402 from biomni_react.schema import TaskSpec # noqa: E402 def load_dotenv_files(paths: list[Path]) -> None: for path in paths: if not path.exists(): continue for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) def load_eval(eval_name: str) -> labbench.Evaluator: return labbench.Evaluator(labbench.Eval(eval_name), debug=False, open_answer=False, use_hf=False) def select_instances( *, eval_name: str, split: str, dev_size: int, test_size: int, seed: int, shard_index: int, shard_count: int, debug: bool, ) -> list[tuple[str, Any]]: evaluator = load_eval(eval_name) instances = list(evaluator.eval_set.instances) rng = random.Random(f"{seed}:{eval_name}:question-set") rng.shuffle(instances) if debug: selected = instances[: min(3, len(instances))] elif split == "dev": selected = instances[: min(dev_size, len(instances))] elif split == "test": start = min(dev_size, len(instances)) selected = instances[start : min(start + test_size, len(instances))] else: selected = instances if shard_count > 1: total = len(selected) chunk_size = (total + shard_count - 1) // shard_count selected = selected[min(total, shard_index * chunk_size) : min(total, (shard_index + 1) * chunk_size)] return selected def load_completed_results(path: Path) -> tuple[set[str], set[str]]: if not path.exists(): return set(), set() completed_questions: set[str] = set() completed_task_ids: set[str] = set() for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines(): if not raw_line.strip(): continue try: record = json.loads(raw_line) except json.JSONDecodeError: continue task_id = str(record.get("task_id") or "").strip() if task_id: completed_task_ids.add(task_id) question = str(record.get("question") or "").strip() if question: completed_questions.add(question) return completed_task_ids, completed_questions def append_text_locked(path: Path, text: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: fcntl.flock(handle.fileno(), fcntl.LOCK_EX) handle.write(text) handle.flush() os.fsync(handle.fileno()) fcntl.flock(handle.fileno(), fcntl.LOCK_UN) def append_jsonl_locked(path: Path, payload: dict[str, Any]) -> None: append_text_locked(path, json.dumps(payload, ensure_ascii=False, default=str) + "\n") def parse_answer(text: str, n_choices: int) -> str: valid = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:n_choices]) if match := ANSWER_RE.search(text or ""): letter = match.group(1).upper() if letter in valid: return letter if match := SOLUTION_RE.search(text or ""): return parse_answer(match.group(1), n_choices) for match in LETTER_RE.finditer(text or ""): letter = match.group(1).upper() if letter in valid: return letter return "" def build_objective(input_obj: Any, eval_name: str) -> str: choices = "\n".join(input_obj.choices) return f""" Answer this multiple-choice LAB-Bench biology question from {eval_name}. Question: {input_obj.question} Options: {choices} Return the single correct letter. You must write answer.txt containing exactly: [ANSWER]X[/ANSWER] where X is one answer letter. Finish with [ANSWER]X[/ANSWER]. """.strip() def make_agent_config(args: argparse.Namespace) -> AgentConfig: api_key = ( args.api_key or os.getenv("BIOMNI_REACT_API_KEY") or os.getenv("DEEPSEEK_API_KEY") or os.getenv("BIOMNI_CUSTOM_API_KEY") or os.getenv("OPENAI_API_KEY") ) if not api_key: raise SystemExit("Missing API key. Set DEEPSEEK_API_KEY, BIOMNI_REACT_API_KEY, BIOMNI_CUSTOM_API_KEY, or OPENAI_API_KEY.") return AgentConfig( model=args.model, base_url=args.base_url, api_key=api_key, max_iterations=args.max_iterations, retrieval_top_k=args.top_k, command_timeout_s=args.command_timeout, ) def run_one( *, agent: BiomniReActAgent, eval_name: str, split: str, subset: str, instance: Any, output_root: Path, result_file: Path, reasoning_log: Path, method: str, model: str, ) -> dict[str, Any]: input_obj, target_output, _unsure = instance.get_input_output() case_dir = output_root / "case_workspaces" / f"{eval_name.lower()}_{instance.id}" answer_path = case_dir / "answer.txt" task = TaskSpec( name=f"LAB-Bench {eval_name} {instance.id}", objective=build_objective(input_obj, eval_name), workspace=case_dir, expected_outputs=[answer_path], constraints=[ "Do not use external network resources.", "Do not inspect answer keys or previous result files.", "Write exactly one answer letter wrapped in [ANSWER] and [/ANSWER].", ], metadata={ "eval": eval_name, "split": split, "subset": subset, "question_id": str(instance.id), }, ) try: result = agent.run(task) raw_output = result.final_answer if answer_path.exists(): raw_output = answer_path.read_text(encoding="utf-8", errors="replace") + "\n" + raw_output error = result.error or "" except Exception as exc: raw_output = "" error = repr(exc) answer = parse_answer(raw_output, len(input_obj.choices)) record = { "task_id": str(instance.id), "subset": subset, "question": str(input_obj.question), "answer": str(target_output), "agent_answer": answer, "method": method, "model": model, "error": error, } append_jsonl_locked(result_file, record) append_text_locked( reasoning_log, "\n".join( [ "=" * 80, f"eval: {eval_name}", f"split: {split}", f"id: {instance.id}", f"subset: {subset}", f"answer: {target_output}", f"agent_answer: {answer}", f"workspace: {case_dir}", f"error: {error}", "", "[raw_output]", raw_output, "", ] ), ) return record def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run LAB-Bench with Biomni-ReAct.") parser.add_argument("--eval", choices=[member.value for member in labbench.Eval], default="DbQA") parser.add_argument("--split", choices=["dev", "test", "all"], default="test") parser.add_argument("--dev-size", type=int, default=DEFAULT_DEV_SIZE) parser.add_argument("--test-size", type=int, default=DEFAULT_TEST_SIZE) parser.add_argument("--seed", type=int, default=DEFAULT_SEED) parser.add_argument("--shard-index", type=int, default=0) parser.add_argument("--shard-count", type=int, default=1) parser.add_argument("--output-root", type=Path, default=DEFAULT_OUTPUT_ROOT) parser.add_argument("--result-file", type=Path, default=None) parser.add_argument("--reasoning-log", type=Path, default=None) parser.add_argument("--skip-existing-results", action="store_true") parser.add_argument("--debug", action="store_true") parser.add_argument("--model", default=os.getenv("BIOMNI_REACT_MODEL", os.getenv("DEEPSEEK_MODEL_NAME", "deepseek-chat"))) parser.add_argument("--base-url", default=os.getenv("BIOMNI_REACT_BASE_URL", os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"))) parser.add_argument("--api-key", default=None) parser.add_argument("--max-iterations", type=int, default=int(os.getenv("BIOMNI_REACT_MAX_ITERATIONS", "8"))) parser.add_argument("--top-k", type=int, default=int(os.getenv("BIOMNI_REACT_TOP_K", "8"))) parser.add_argument("--command-timeout", type=int, default=int(os.getenv("BIOMNI_REACT_TIMEOUT", "120"))) parser.add_argument("--env-file", action="append", type=Path, default=[]) return parser.parse_args() def main() -> int: args = parse_args() if args.shard_count < 1: raise SystemExit("--shard-count must be at least 1.") if args.shard_index < 0 or args.shard_index >= args.shard_count: raise SystemExit("--shard-index must satisfy 0 <= shard-index < shard-count.") load_dotenv_files([PROJECT_ROOT / ".env", LAB_BENCH_ROOT / ".env", Path("/225040511/project/.env"), *args.env_file]) args.output_root.mkdir(parents=True, exist_ok=True) eval_lower = args.eval.lower() result_file = args.result_file or args.output_root / f"{eval_lower}_results.jsonl" reasoning_log = args.reasoning_log or args.output_root / f"{eval_lower}_reasoning.log" selected = select_instances( eval_name=args.eval, split=args.split, dev_size=args.dev_size, test_size=args.test_size, seed=args.seed, shard_index=args.shard_index, shard_count=args.shard_count, debug=args.debug, ) if args.skip_existing_results: completed_task_ids, completed_questions = load_completed_results(result_file) selected = [ (subset, instance) for subset, instance in selected if str(instance.id) not in completed_task_ids and str(instance.get_input_output()[0].question).strip() not in completed_questions ] config = make_agent_config(args) agent = BiomniReActAgent(config=config) for subset, instance in selected: record = run_one( agent=agent, eval_name=args.eval, split=args.split, subset=subset, instance=instance, output_root=args.output_root, result_file=result_file, reasoning_log=reasoning_log, method="Biomni-ReAct", model=config.model, ) print(json.dumps(record, ensure_ascii=False), flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())