{
  "name": "kinase-questions",
  "display_name": "Kinase questions",
  "summary": "Guessing game for one human kinase. One clue per round. Eureka keeps the answer and the score. A few inexpensive model calls, and no virtual machine.",
  "steps": [
    {
      "id": "s1",
      "kind": "slurm-cpu",
      "name": "referee",
      "script": "jobs/s1.sh",
      "after": [],
      "use_slurm": false,
      "durationHours": "",
      "instanceType": "",
      "container": "",
      "copies": [],
      "failOnError": true,
      "shape": {
        "cpus": 2,
        "mem": "4G",
        "nodes": 1
      }
    },
    {
      "id": "s2",
      "kind": "ai-generate",
      "name": "question",
      "after": [
        "s1"
      ],
      "modelFamily": "cheap",
      "model": "",
      "failOnError": true,
      "script": "prompts/s2.txt",
      "input": {
        "kind": "file",
        "path": "prompts/s2.txt"
      },
      "outputPath": ""
    }
  ],
  "spans": [
    {
      "id": "ask",
      "name": "ask",
      "mode": "repeat",
      "from": "s1",
      "to": "s2",
      "maxTurns": 6,
      "checkStage": "s1",
      "checkFile": "score.json",
      "stopWhen": {
        "all": [
          {
            "check": "number",
            "path": "confidence",
            "op": "gte",
            "value": 0.8
          }
        ]
      }
    }
  ],
  "files": {
    "jobs/s1.sh": "#!/bin/bash\nset -euo pipefail\npython3 - <<'PY'\nimport json, os, re\n\ninn = os.environ.get(\"EUREKA_IN\") or \".\"\nout = os.environ.get(\"EUREKA_OUT\") or \".\"\nos.makedirs(out, exist_ok=True)\n\nORDER = (\"family\", \"symbol\", \"isoform\", \"accession\")\nOTHERS = (\"family\", \"symbol\", \"isoform\")\nLINES = {\n    \"family\": \"family: human serine/threonine protein kinase\",\n    \"symbol\": \"symbol: AKT\",\n    \"isoform\": \"isoform: alpha\",\n    \"accession\": \"accession: P31749 AKT1_HUMAN\",\n}\n\ndef read(path):\n    try:\n        with open(path, encoding=\"utf-8\") as handle:\n            return handle.read()\n    except OSError:\n        return \"\"\n\ndef first_object(text):\n    body = (text or \"\").strip()\n    if body.startswith(\"```\"):\n        lines = body.splitlines()\n        if lines and lines[0].startswith(\"```\"):\n            lines = lines[1:]\n        if lines and lines[-1].strip() == \"```\":\n            lines = lines[:-1]\n        body = \"\\n\".join(lines).strip()\n    start = body.find(\"{\")\n    if start < 0:\n        return {}\n    try:\n        obj, _end = json.JSONDecoder().raw_decode(body[start:])\n    except json.JSONDecodeError:\n        return {}\n    return obj if isinstance(obj, dict) else {}\n\ndef known_from(text):\n    found = set()\n    for line in (text or \"\").splitlines():\n        head = line.strip().lower()\n        for word in ORDER:\n            if head.startswith(word + \":\"):\n                found.add(word)\n    return found\n\ndef one_ask(raw):\n    text = re.sub(r\"[^a-z]\", \" \", str(raw or \"\").lower())\n    hits = [word for word in ORDER if re.search(r\"\\b%s\\b\" % word, text)]\n    if len(hits) == 1:\n        return hits[0]\n    return \"\"\n\nknown = known_from(read(os.path.join(inn, \"from-s1\", \"transcript.txt\")))\nreply = first_object(read(os.path.join(inn, \"from-s2\", \"reply.txt\")))\nask = one_ask(reply.get(\"ask\"))\nif ask in OTHERS and ask not in known:\n    known.add(ask)\nelif ask == \"accession\" and \"accession\" not in known and len(known & set(OTHERS)) >= 2:\n    known.add(\"accession\")\n\nothers = len(known & set(OTHERS))\nif \"accession\" in known:\n    cap = 0.95\nelif others >= 2:\n    cap = 0.6\nelif others == 1:\n    cap = 0.4\nelse:\n    cap = 0.2\ncap_text = {0.2: \"0.2\", 0.4: \"0.4\", 0.6: \"0.6\", 0.95: \"0.95\"}[cap]\nshown = [LINES[word] for word in ORDER if word in known]\ntranscript = (\"\\n\".join(shown) + \"\\n\") if shown else \"facts: none\\n\"\n\ndef put(name, text):\n    with open(os.path.join(out, name), \"w\", encoding=\"utf-8\") as handle:\n        handle.write(text)\n\nput(\"transcript.txt\", transcript)\nput(\"cap.txt\", cap_text + \"\\n\")\nput(\"score.json\", json.dumps({\"confidence\": cap}) + \"\\n\")\ngene = \"AKT1_HUMAN\\n\" if \"accession\" in known else \"unknown\\n\"\nput(\"gene.txt\", gene)\nput(\"asked.txt\", (ask or \"none\") + \"\\n\")\nPY\n",
    "prompts/s2.txt": "You are identifying one human protein kinase. The referee answers one question per turn and writes the score. You do not.\n\nReply with one JSON object and no markdown. Fields:\n- ask: exactly one of family, symbol, isoform, accession\n- guess: copy the text in gene.txt. If that file says unknown, guess is unknown. Do not copy any other token.\n- why: one short sentence\n- confidence: copy the number in cap.txt. The referee ignores this number. A larger number does not pass the turn.\n\nHow to ask:\n- If the transcript has no family line, ask family.\n- If it has family and no symbol line, ask symbol.\n- If it has family and symbol and no accession line, ask accession.\n- Ask accession only after two other answers are already in the transcript. The referee will not answer it before that.\n- A repeated question, or any ask outside those four words, adds no fact.\n- When gene.txt is no longer unknown, copy it into guess and leave ask as accession.\n\nRead the referee files below. The last check is the referee's score, not a field for you to correct.\n"
  }
}
