Spaces:
Paused
Paused
| """Deterministic answer extraction and response normalization. | |
| Bypasses the model for questions where the answer can be computed | |
| directly from retrieved evidence (exon counts, domain matching, NMD | |
| prediction, variant-specific therapy lookups). | |
| """ | |
| import re | |
| import time | |
| from difflib import SequenceMatcher | |
| from src.equivalence import canonical_drug_name, find_equivalent_in_choices | |
| from src.dags import clinical_trials as ct_dag | |
| from src.dags import variant_assessment as va_dag | |
| def parse_answer_choices(question_text: str) -> list[str]: | |
| m = re.search(r"answer choices:\s*(.+)$", question_text, flags=re.IGNORECASE) | |
| if not m: | |
| return [] | |
| raw = m.group(1).strip().rstrip(".") | |
| return [c.strip() for c in raw.split(",") if c.strip()] | |
| def _best_choice_match(text: str, choices: list[str]) -> str | None: | |
| if not choices: | |
| return None | |
| text_norm = text.strip().lower() | |
| for c in choices: | |
| if text_norm == c.strip().lower(): | |
| return c | |
| for c in choices: | |
| c_norm = c.strip().lower() | |
| if c_norm in text_norm or text_norm in c_norm: | |
| return c | |
| scored = [(SequenceMatcher(None, text_norm, c.strip().lower()).ratio(), c) for c in choices] | |
| scored.sort(reverse=True, key=lambda x: x[0]) | |
| if scored and scored[0][0] >= 0.55: | |
| return scored[0][1] | |
| return None | |
| def _extract_numeric(text: str) -> str | None: | |
| m = re.search(r"(-?\d+(?:\.\d+)?)", text) | |
| return m.group(1) if m else None | |
| def normalize_response(response: str, input_data: dict, evidence: list[dict]) -> str: | |
| answer_format = input_data["question"]["answer_format"] | |
| question_text = input_data["question"]["prompt"] | |
| text = response.strip().strip('"').strip("'").strip() | |
| if answer_format == "binary": | |
| low = text.lower() | |
| if low in {"yes", "y", "true", "eligible"}: | |
| return "Yes" | |
| if low in {"no", "n", "false", "ineligible"}: | |
| return "No" | |
| if "yes" in low: | |
| return "Yes" | |
| if "no" in low: | |
| return "No" | |
| return text | |
| if answer_format == "numeric_match": | |
| n = _extract_numeric(text) | |
| return n if n is not None else text | |
| if answer_format == "string_match": | |
| if "clinical trial" in question_text.lower() or "nct" in question_text.lower(): | |
| trial = re.search(r"\bNCT\d{6,}\b", text, flags=re.IGNORECASE) | |
| if trial: | |
| return trial.group(0).upper() | |
| for e in evidence: | |
| m = re.search(r"\bNCT\d{6,}\b", e.get("snippet", ""), flags=re.IGNORECASE) | |
| if m: | |
| return m.group(0).upper() | |
| return text | |
| if answer_format == "multiple_choice": | |
| choices = parse_answer_choices(question_text) | |
| matched = _best_choice_match(text, choices) | |
| if matched: | |
| return matched | |
| # Equivalence fallback: did the model produce a clinically equivalent drug? | |
| eq = find_equivalent_in_choices(text, choices) | |
| if eq: | |
| return eq | |
| return text | |
| return text | |
| def _extract_variant_position(input_data: dict) -> int | None: | |
| prompt = input_data["question"]["prompt"] | |
| m = re.search(r"position\s+(\d+)", prompt, flags=re.IGNORECASE) | |
| if m: | |
| return int(m.group(1)) | |
| protein = input_data["patient"]["genotype"][0].get("variant_protein", "") | |
| m = re.search(r"p\.\(?[A-Za-z*]+(\d+)", protein) | |
| if m: | |
| return int(m.group(1)) | |
| return None | |
| def _parse_uniprot_domains(snippet: str) -> list[tuple[str, int, int]]: | |
| domains = [] | |
| for m in re.finditer(r"(?:Domain|Region):\s*(.*?)\s*\(aa\s*(\d+)-(\d+)\)", snippet): | |
| domains.append((m.group(1).strip(), int(m.group(2)), int(m.group(3)))) | |
| return domains | |
| def try_direct_answer(input_data: dict, evidence: list[dict]) -> dict | None: | |
| """Return a deterministic answer if one can be computed from evidence.""" | |
| category = input_data["question"]["category"] | |
| answer_format = input_data["question"]["answer_format"] | |
| prompt = input_data["question"]["prompt"].lower() | |
| # ---------------------------------------------------------------- | |
| # Tier 1.5: Reasoning DAGs for Clinical_Trials and Variant_Assessment — | |
| # explicit sub-question decomposition and composition. | |
| # ---------------------------------------------------------------- | |
| # Clinical_Trials DAG | |
| if category == "Clinical_Trials": | |
| result = ct_dag.evaluate_with_meta(input_data, evidence) | |
| if result.resolved: | |
| return _make_payload(result.value, _stub_evidence(result), | |
| result.justification[:500]) | |
| # Variant_Assessment: try DAGs in priority order | |
| if category == "Variant_Assessment": | |
| # Numeric questions (amino-acid count / fraction) | |
| if answer_format == "numeric_match": | |
| r = va_dag.NUMERIC_DAG.evaluate(input_data, evidence) | |
| if r.resolved: | |
| return _make_payload(r.value, _stub_evidence(r), | |
| r.justification[:500]) | |
| # NMD binary | |
| if answer_format == "binary" and "nonsense mediated decay" in prompt: | |
| r = va_dag.NMD_DAG.evaluate(input_data, evidence) | |
| if r.resolved: | |
| return _make_payload(r.value, _stub_evidence(r), | |
| r.justification[:500]) | |
| # Domain matching | |
| if answer_format == "multiple_choice": | |
| r = va_dag.DOMAIN_DAG.evaluate(input_data, evidence) | |
| if r.resolved: | |
| return _make_payload(r.value, _stub_evidence(r), | |
| r.justification[:500]) | |
| # Pre-computed variant-specific therapy lookups | |
| for e in evidence: | |
| if e.get("source_name") == "Variant_Therapy_Lookup": | |
| answer = e.get("_precomputed_answer", "") | |
| if answer: | |
| if answer_format == "multiple_choice": | |
| choices = parse_answer_choices(input_data["question"]["prompt"]) | |
| if choices: | |
| matched = _best_choice_match(answer, choices) | |
| if matched: | |
| answer = matched | |
| return _make_payload(answer, e, e.get("_mechanism", "")[:500]) | |
| # Pre-computed supportive-care lookups (EDS-overlap connective tissue disorders) | |
| for e in evidence: | |
| if e.get("source_name") == "Supportive_Care_Lookup": | |
| answer = e.get("_precomputed_answer", "") | |
| if answer: | |
| return _make_payload(answer, e, e.get("_mechanism", "")[:500]) | |
| # Clinical Trials: deterministic eligibility filter. | |
| # If every retrieved trial fails at least one eligibility gate (not testing a | |
| # new therapeutic, observational only, or patient age out of range), the | |
| # answer is "None". | |
| if (category == "Clinical_Trials" | |
| and answer_format == "string_match"): | |
| trial_evidence = [e for e in evidence if e.get("source_name") == "ClinicalTrials.gov"] | |
| asks_new_therapeutic = any(kw in prompt for kw in ( | |
| "new therapeutic", "new treatment", "novel therap", | |
| "new therapy", "investigational", | |
| )) | |
| def _eligible(e: dict) -> bool: | |
| if asks_new_therapeutic and not e.get("_is_new_therapeutic", True): | |
| return False | |
| if asks_new_therapeutic and e.get("_is_observational", False): | |
| return False | |
| if not e.get("_age_eligible", True): | |
| return False | |
| return True | |
| if trial_evidence: | |
| eligible_trials = [e for e in trial_evidence if _eligible(e)] | |
| if not eligible_trials: | |
| reasons = [] | |
| if asks_new_therapeutic and all(not e.get("_is_new_therapeutic", True) for e in trial_evidence): | |
| reasons.append("all retrieved trials test only already-FDA-approved drugs") | |
| if all(e.get("_is_observational", False) for e in trial_evidence) and asks_new_therapeutic: | |
| reasons.append("all retrieved trials are observational") | |
| if all(not e.get("_age_eligible", True) for e in trial_evidence): | |
| reasons.append("patient age outside trial enrollment window") | |
| why = "; ".join(reasons) if reasons else "no retrieved trial meets eligibility criteria" | |
| return _make_payload( | |
| "None", trial_evidence[0], | |
| f"No eligible trial: {why}.", | |
| ) | |
| # Pre-computed DMD exon-skipping lookup | |
| for e in evidence: | |
| if e.get("source_name") == "DMD_ExonSkip_Lookup": | |
| dmd = e.get("_dmd_result", {}) | |
| drug = dmd.get("amenable_drug", "") | |
| if drug: | |
| if answer_format == "multiple_choice": | |
| choices = parse_answer_choices(input_data["question"]["prompt"]) | |
| if choices: | |
| matched = _best_choice_match(drug, choices) | |
| if matched: | |
| drug = matched | |
| return _make_payload(drug, e, dmd.get("mechanism", "")[:500]) | |
| if category != "Variant_Assessment": | |
| return None | |
| # Extract answers from Ensembl evidence | |
| for e in evidence: | |
| snippet = e.get("snippet", "") | |
| low = snippet.lower() | |
| if answer_format == "numeric_match": | |
| if "amino acids" in prompt: | |
| m = re.search(r"coding\s+(\d+)\s+amino acids", low) | |
| if m: | |
| return _make_payload(m.group(1), e, "Amino acid count from Ensembl exon mapping.") | |
| if "percentage" in prompt or "decimal" in prompt or "fraction" in prompt: | |
| m = re.search(r"fraction of total protein:\s*\d+/\d+\s*=\s*([0-9]*\.?[0-9]+)", low) | |
| if m: | |
| return _make_payload(m.group(1), e, "Exon fraction from Ensembl evidence.") | |
| if answer_format == "binary" and "nonsense mediated decay" in prompt: | |
| if "trigger nmd" in low: | |
| return _make_payload("Yes", e, "Variant in early exon predicted to trigger NMD.") | |
| if "escape nmd" in low: | |
| return _make_payload("No", e, "Variant in last/penultimate exon predicted to escape NMD.") | |
| # Domain matching from UniProt evidence. | |
| # | |
| # When multiple UniProt annotations contain the variant position and several | |
| # appear among the answer choices, select with this preference order: | |
| # 1. Among choice-matching candidates (score >= 0.85), prefer the earliest | |
| # start position. UniProt returns features sorted by start, and the | |
| # earliest annotation containing a position is typically its most | |
| # specific canonical functional unit. | |
| # 2. Tiebreaker: prefer typed `Domain` over typed `Region` (typed Domain | |
| # annotations are the most authoritative functional units). | |
| # 3. Tiebreaker: prefer the broader span (larger end-start) — broader | |
| # structural classifications often supersede narrower sub-features. | |
| if answer_format == "multiple_choice": | |
| choices = parse_answer_choices(input_data["question"]["prompt"]) | |
| if choices: | |
| position = _extract_variant_position(input_data) | |
| for e in evidence: | |
| if e.get("source_name") != "UniProt": | |
| continue | |
| snippet = e.get("snippet", "") | |
| # Extract ALL domains and their type from the snippet. | |
| # _parse_uniprot_domains returns (name, start, end). We need | |
| # type too — re-parse with the typed regex. | |
| typed = re.findall( | |
| r"(Domain|Region):\s*(.*?)\s*\(aa\s*(\d+)-(\d+)\)", | |
| snippet, | |
| ) | |
| if not typed and position is None: | |
| continue | |
| # Build the typed list and filter to those containing the position | |
| if position is not None: | |
| typed_containing = [ | |
| {"type": t, "name": n.strip(), "start": int(s), "end": int(e_)} | |
| for t, n, s, e_ in typed | |
| if int(s) <= position <= int(e_) | |
| ] | |
| else: | |
| typed_containing = [] | |
| if typed_containing: | |
| # Score each containing annotation against every answer choice. | |
| # Keep only "candidates" with a strong match (>= 0.85). | |
| candidates = [] | |
| for ann in typed_containing: | |
| for choice in choices: | |
| score = SequenceMatcher( | |
| None, ann["name"].lower(), choice.lower() | |
| ).ratio() | |
| if score >= 0.85: | |
| candidates.append({ | |
| "ann": ann, | |
| "choice": choice, | |
| "score": score, | |
| }) | |
| if candidates: | |
| # When both a typed Domain AND a Region match the answer | |
| # choices, AND the Domain is much broader than the Region | |
| # (>2x span), prefer the Region — it's a more specific | |
| # functional sub-feature within the broader Domain. | |
| # (Handles LMNA-like cases: IF rod 356bp Domain vs Coil 1B | |
| # 137bp Region; the Region is the right answer.) | |
| domain_cands = [c for c in candidates if c["ann"]["type"] == "Domain"] | |
| region_cands = [c for c in candidates if c["ann"]["type"] == "Region"] | |
| if domain_cands and region_cands: | |
| d_span = domain_cands[0]["ann"]["end"] - domain_cands[0]["ann"]["start"] | |
| r_span = region_cands[0]["ann"]["end"] - region_cands[0]["ann"]["start"] | |
| if d_span > 2 * r_span: | |
| # Prefer the more-specific Region | |
| region_cands.sort(key=lambda c: ( | |
| c["ann"]["start"], | |
| -(c["ann"]["end"] - c["ann"]["start"]), | |
| -c["score"], | |
| )) | |
| best = region_cands[0] | |
| return _make_payload( | |
| best["choice"], e, | |
| f"Position {position} maps to Region {best['ann']['name']} " | |
| f"(aa {best['ann']['start']}-{best['ann']['end']}). " | |
| f"Region selected as more specific than the broader containing Domain.", | |
| ) | |
| # Default ordering: | |
| # 1. Earliest start position | |
| # 2. Domain type: typed `Domain` before `Region` | |
| # 3. Larger span (broader category) | |
| # 4. Higher string-match score | |
| type_rank = {"Domain": 0, "Region": 1} | |
| candidates.sort(key=lambda c: ( | |
| c["ann"]["start"], | |
| type_rank.get(c["ann"]["type"], 9), | |
| -(c["ann"]["end"] - c["ann"]["start"]), | |
| -c["score"], | |
| )) | |
| best = candidates[0] | |
| return _make_payload( | |
| best["choice"], e, | |
| f"Position {position} maps to {best['ann']['type']}: " | |
| f"{best['ann']['name']} " | |
| f"(aa {best['ann']['start']}-{best['ann']['end']}). " | |
| f"Selected by earliest-start preference among answer-choice candidates.", | |
| ) | |
| # No high-confidence candidate — fall back to looser matching | |
| # over containing annotations only (not the entire domain list). | |
| best_choice = None | |
| best_score = -1.0 | |
| best_ann = None | |
| for ann in typed_containing: | |
| for choice in choices: | |
| score = SequenceMatcher( | |
| None, ann["name"].lower(), choice.lower() | |
| ).ratio() | |
| if score > best_score: | |
| best_score = score | |
| best_choice = choice | |
| best_ann = ann | |
| if best_choice and best_score >= 0.4: | |
| return _make_payload( | |
| best_choice, e, | |
| f"Position {position} maps to {best_ann['name']}. " | |
| f"Matched answer choice: {best_choice} (score={best_score:.2f}).", | |
| ) | |
| # Fallback: use the primary DOMAIN MATCH line | |
| dm = re.search( | |
| r"DOMAIN MATCH: amino acid position \d+ falls in (?:Domain|Region): (.+?) \(aa \d+-\d+\)", | |
| snippet, | |
| ) | |
| if dm: | |
| best = _best_choice_match(dm.group(1).strip(), choices) | |
| if best: | |
| return _make_payload(best, e, f"Pre-computed domain match: {dm.group(1).strip()}.") | |
| return None | |
| def _make_payload(response: str, evidence_item: dict, justification: str) -> dict: | |
| return { | |
| "response": response, | |
| "evidence": [ | |
| { | |
| "source": evidence_item.get("url", ""), | |
| "time_accessed": int(time.time()), | |
| "justification": justification[:500], | |
| } | |
| ], | |
| } | |
| def _stub_evidence(node_result) -> dict: | |
| """Convert a reasoning_dag.NodeResult into the evidence-dict shape | |
| `_make_payload` expects.""" | |
| return {"url": getattr(node_result, "evidence_url", "")} | |