from __future__ import annotations import json import re from typing import Any _JSON_FENCE_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.IGNORECASE | re.DOTALL) _CONSOLIDATED_FINDINGS_MARKER = "Consolidated list of findings:" def _loads_first_json(candidate: str) -> Any: decoder = json.JSONDecoder() stripped = candidate.strip() if not stripped: raise ValueError("JSON candidate is empty.") for index, char in enumerate(stripped): if char not in "{[": continue try: payload, _ = decoder.raw_decode(stripped, index) return payload except json.JSONDecodeError: continue raise ValueError("Could not parse a JSON value from the candidate text.") def _scan_json_values(text: str) -> list[Any]: decoder = json.JSONDecoder() values: list[Any] = [] for index, char in enumerate(text): if char not in "{[": continue try: payload, _ = decoder.raw_decode(text, index) except json.JSONDecodeError: continue values.append(payload) return values def _is_finding_item(value: Any) -> bool: if not isinstance(value, dict): return False return all(key in value for key in ("finding", "anatomical_location", "certainty")) def _is_finding_list(value: Any) -> bool: if not isinstance(value, list): return False return all(_is_finding_item(item) for item in value) def _normalize_discovery_candidate(payload: Any) -> dict[str, Any] | None: if isinstance(payload, dict) and "findings" in payload: if isinstance(payload["findings"], list): return payload return None if _is_finding_list(payload): return {"findings": payload} if _is_finding_item(payload): return None return None def _has_structured_discovery_payload(values: list[Any]) -> bool: for value in values: if isinstance(value, dict) and "findings" in value: return True if _is_finding_list(value): return True return False def strip_medgemma_thinking_trace(response: str) -> str: """Remove the notebook-documented MedGemma thinking trace when present.""" if "" in response: return response.split("", 1)[1].lstrip() return response.strip() def extract_finding_discovery_payload(response: str) -> dict[str, Any]: cleaned = strip_medgemma_thinking_trace(response).strip() scanned_values = _scan_json_values(cleaned) standalone_finding_dicts = [ value for value in scanned_values if _is_finding_item(value) ] if len(standalone_finding_dicts) >= 2 and not _has_structured_discovery_payload( scanned_values ): raise ValueError( "Could not extract finding discovery JSON from MedGemma response. " f"Response preview: {cleaned[:500]!r}" ) try: full_payload = json.loads(cleaned) except json.JSONDecodeError: full_payload = None else: normalized = _normalize_discovery_candidate(full_payload) if normalized is not None: return normalized for value in scanned_values: if isinstance(value, dict) and "findings" in value: normalized = _normalize_discovery_candidate(value) if normalized is not None: return normalized if _CONSOLIDATED_FINDINGS_MARKER in cleaned: after_marker = cleaned.split(_CONSOLIDATED_FINDINGS_MARKER, 1)[1].strip() try: marker_payload = _loads_first_json(after_marker) except ValueError: marker_payload = None else: normalized = _normalize_discovery_candidate(marker_payload) if normalized is not None: return normalized for match in _JSON_FENCE_PATTERN.finditer(cleaned): fenced_text = match.group(1).strip() try: fenced_payload = _loads_first_json(fenced_text) except ValueError: continue normalized = _normalize_discovery_candidate(fenced_payload) if normalized is not None: return normalized for value in scanned_values: if _is_finding_list(value): return {"findings": value} raise ValueError( "Could not extract finding discovery JSON from MedGemma response. " f"Response preview: {cleaned[:500]!r}" ) def extract_json_payload(response: str, expected_type: type) -> Any: cleaned = strip_medgemma_thinking_trace(response) candidates: list[str] = [ match.group(1).strip() for match in _JSON_FENCE_PATTERN.finditer(cleaned) ] if not candidates: if expected_type is dict: start = cleaned.find("{") end = cleaned.rfind("}") elif expected_type is list: start = cleaned.find("[") end = cleaned.rfind("]") else: raise TypeError("expected_type must be dict or list.") if start == -1 or end == -1 or end <= start: raise ValueError("Could not locate a JSON payload in the model response.") candidates.append(cleaned[start : end + 1]) errors: list[str] = [] for candidate in candidates: try: payload = _loads_first_json(candidate) except ValueError as error: errors.append(str(error)) continue if isinstance(payload, expected_type): return payload errors.append( f"expected {expected_type.__name__}, received {type(payload).__name__}" ) detail = "; ".join(errors) if errors else "no JSON candidates were found" raise ValueError( f"Could not extract JSON {expected_type.__name__} from the model response: {detail}" )