Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/medcheck/llm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,23 @@ class AnalysisResult:


def parse_llm_json(raw: str) -> dict[str, Any]:
"""Extract and parse the first valid JSON object from *raw* using brace-depth tracking."""
start = raw.index("{")
depth = 0
for i, ch in enumerate(raw[start:], start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
result: dict[str, Any] = json.loads(raw[start : i + 1])
"""Extract and parse the first JSON object embedded in *raw*.

Uses json's incremental decoder — which is string/escape aware — rather
than manual brace counting, so a ``}`` inside a string value (plausible in
LLM free-text fields) cannot truncate the object. Candidate ``{`` positions
are tried in order until one decodes to an object.
"""
decoder = json.JSONDecoder()
pos = raw.find("{")
while pos != -1:
try:
result, _ = decoder.raw_decode(raw, pos)
except json.JSONDecodeError:
result = None
if isinstance(result, dict):
return result
pos = raw.find("{", pos + 1)
raise ValueError("No valid JSON found")


Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_llm/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,26 @@ def fn():
call_with_retries(fn, provider="x", attempts=3, base_delay=0)
# Non-transient error: no retries.
assert calls["n"] == 1


def test_parse_llm_json_braces_inside_string_values():
raw = (
'{"overall_impression": "Normal study (note: {contrast} was not administered)",'
' "structures": [{"name": "ACL", "status": "intact", "confidence": 0.9}]}'
)
result = parse_llm_response(raw)
assert "not administered" in result.overall_impression
assert len(result.structures) == 1
assert result.structures[0].name == "ACL"


def test_parse_llm_json_object_after_stray_brace():
raw = 'Header {not json. {"overall_impression": "ok", "structures": []}'
result = parse_llm_response(raw)
assert result.overall_impression == "ok"


def test_parse_llm_json_markdown_fenced():
raw = 'Here is the analysis:\n```json\n{"overall_impression": "unremarkable", "structures": []}\n```\nDone.'
result = parse_llm_response(raw)
assert result.overall_impression == "unremarkable"
Loading