| from __future__ import annotations |
|
|
| import argparse |
| import asyncio |
| import fcntl |
| import json |
| import os |
| import random |
| import re |
| import sys |
| import types |
| from collections import defaultdict |
| from pathlib import Path |
| from typing import Any |
|
|
| from langchain_core.messages import HumanMessage, SystemMessage |
| from langchain_openai import ChatOpenAI |
|
|
| LAB_BENCH_ROOT = Path("/225040511/project/LAB-Bench") |
| PROJECT_ROOT = Path("/225040511/project/react_code_bioagent_deepseek") |
| DEFAULT_OUTPUT_ROOT = PROJECT_ROOT / "labbench_runs" / "base_llm" |
| DEFAULT_RESULT_FILE = DEFAULT_OUTPUT_ROOT / "base_llm_results.jsonl" |
| DEFAULT_REASONING_LOG = DEFAULT_OUTPUT_ROOT / "base_llm_reasoning.log" |
| DEFAULT_EVALS = ("DbQA", "SeqQA") |
| DEFAULT_DEV_SIZE = 45 |
| DEFAULT_TEST_SIZE = 315 |
| DEFAULT_SEED = 20260514 |
|
|
| ANSWER_RE = re.compile(r"\[ANSWER\]\s*([A-Z])\s*\[/ANSWER\]", re.IGNORECASE) |
| LETTER_RE = re.compile(r"\b([A-Z])\b", re.IGNORECASE) |
|
|
| sys.path.insert(0, str(LAB_BENCH_ROOT)) |
|
|
|
|
| def install_labbench_import_stubs() -> None: |
| """Stub optional provider packages needed only while importing LAB-Bench.""" |
| 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 |
|
|
|
|
| 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 make_model(args: argparse.Namespace) -> ChatOpenAI: |
| api_key = args.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_CUSTOM_API_KEY, or OPENAI_API_KEY.") |
| model_name = args.model |
| if ( |
| "deepseek" in args.base_url.lower() |
| and not args.allow_deepseek_reasoner |
| and ("reasoner" in model_name.lower() or "thinking" in model_name.lower()) |
| ): |
| print( |
| f"DeepSeek model {model_name!r} uses thinking/reasoning_content mode; " |
| "falling back to 'deepseek-chat' for LAB-Bench base LLM calls.", |
| flush=True, |
| ) |
| model_name = "deepseek-chat" |
| return ChatOpenAI( |
| model=model_name, |
| api_key=api_key, |
| base_url=args.base_url, |
| temperature=args.temperature, |
| timeout=args.llm_timeout, |
| max_retries=args.max_retries, |
| ) |
|
|
|
|
| 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 |
| for match in LETTER_RE.finditer(text or ""): |
| letter = match.group(1).upper() |
| if letter in valid: |
| return letter |
| return "A" |
|
|
|
|
| def load_eval(eval_name: str) -> labbench.Evaluator: |
| return labbench.Evaluator(labbench.Eval(eval_name), debug=False, open_answer=False, use_hf=False) |
|
|
|
|
| def split_counts(total: int, eval_names: list[str], seed: int) -> dict[str, int]: |
| sizes = {name: len(load_eval(name).eval_set.instances) for name in eval_names} |
| total_available = sum(sizes.values()) |
| if total >= total_available: |
| return sizes |
| raw = {name: total * sizes[name] / total_available for name in eval_names} |
| counts = {name: int(raw[name]) for name in eval_names} |
| remaining = total - sum(counts.values()) |
| rng = random.Random(seed) |
| order = sorted(eval_names, key=lambda name: (raw[name] - counts[name], rng.random()), reverse=True) |
| for name in order[:remaining]: |
| counts[name] += 1 |
| return counts |
|
|
|
|
| def select_instances( |
| evaluator: labbench.Evaluator, |
| *, |
| eval_name: str, |
| split: str, |
| split_size: int, |
| dev_count: int, |
| seed: int, |
| debug: bool, |
| shard_index: int, |
| shard_count: int, |
| ) -> list[tuple[str, Any]]: |
| 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(split_size, len(instances))] |
| elif split == "test": |
| start = min(dev_count, len(instances)) |
| selected = instances[start : min(start + split_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_questions(path: Path | None) -> set[str]: |
| if path is None or not path.exists(): |
| return set() |
| completed: 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 |
| question = str(record.get("question") or "").strip() |
| if question: |
| completed.add(question) |
| return completed |
|
|
|
|
| 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 build_prompt(input_obj: Any, eval_name: str) -> str: |
| choices = "\n".join(input_obj.choices) |
| return f""" |
| The following is a multiple-choice LAB-Bench biology question from {eval_name}. |
| Please answer by responding with the letter of the correct answer. |
| |
| Question: |
| {input_obj.question} |
| |
| Options: |
| {choices} |
| |
| You MUST include the letter of the correct answer within the following tags: [ANSWER] and [/ANSWER]. |
| For example: [ANSWER]A[/ANSWER] |
| Always answer in exactly this format of a single letter between the tags, even if you are unsure. |
| """.strip() |
|
|
|
|
| class BaseLLMLabBenchAgent: |
| def __init__(self, model: ChatOpenAI): |
| self.model = model |
|
|
| async def run_task(self, input_obj: Any, eval_name: str) -> tuple[str, str, str]: |
| prompt = build_prompt(input_obj, eval_name) |
| response = await self.model.ainvoke( |
| [ |
| SystemMessage(content="You are a careful biology benchmark assistant. Return exactly one [ANSWER]X[/ANSWER] tag."), |
| HumanMessage(content=prompt), |
| ] |
| ) |
| raw_output = str(response.content) |
| return parse_answer(raw_output, len(input_obj.choices)), raw_output, prompt |
|
|
|
|
| def compute_metrics(results: list[dict[str, Any]]) -> dict[str, float]: |
| n_total = len(results) |
| n_correct = sum(bool(r["correct"]) for r in results) |
| n_sure = sum(bool(r["sure"]) for r in results) |
| return { |
| "accuracy": n_correct / n_total if n_total else 0.0, |
| "precision": n_correct / n_sure if n_sure else 0.0, |
| "coverage": n_sure / n_total if n_total else 0.0, |
| "n_total": n_total, |
| } |
|
|
|
|
| def compact_record(result: dict[str, Any]) -> dict[str, str]: |
| return { |
| "question": str(result.get("question") or ""), |
| "answer": str(result.get("target_choice") or ""), |
| "agent_answer": str(result.get("agent_output") or ""), |
| } |
|
|
|
|