diff --git a/skillopt/utils/json_utils.py b/skillopt/utils/json_utils.py index 408cb23..8a9be93 100644 --- a/skillopt/utils/json_utils.py +++ b/skillopt/utils/json_utils.py @@ -82,7 +82,6 @@ def _top_level_bracket_arrays(text: str) -> list[str]: i, n = 0, len(text) outer_in_str = False outer_esc = False - brace_depth = 0 while i < n: ch = text[i] if outer_in_str: @@ -99,14 +98,36 @@ def _top_level_bracket_arrays(text: str) -> list[str]: i += 1 continue if ch == "{": - brace_depth += 1 - i += 1 + # Skip balanced objects, including arrays nested inside them. An + # unmatched brace may be ordinary prose, though, so do not let it + # poison the rest of the scan and hide a later valid array. + depth = 0 + in_str = False + esc = False + j = i + while j < n: + current = text[j] + if in_str: + if esc: + esc = False + elif current == "\\": + esc = True + elif current == '"': + in_str = False + elif current == '"': + in_str = True + elif current == "{": + depth += 1 + elif current == "}": + depth -= 1 + if depth == 0: + i = j + 1 + break + j += 1 + else: + i += 1 continue - if ch == "}": - brace_depth = max(0, brace_depth - 1) - i += 1 - continue - if ch != "[" or brace_depth: + if ch != "[": i += 1 continue diff --git a/tests/test_json_utils.py b/tests/test_json_utils.py index 0728dc6..74b4238 100644 --- a/tests/test_json_utils.py +++ b/tests/test_json_utils.py @@ -101,6 +101,10 @@ class TestTopLevelBracketArrays: def test_bracket_inside_quoted_prose_is_ignored(self) -> None: assert _top_level_bracket_arrays('label is "set it to [x]" done') == [] + def test_unmatched_prose_brace_does_not_hide_later_array(self) -> None: + text = "Explanation uses {placeholder, final answer [1, 2]" + assert _top_level_bracket_arrays(text) == ["[1, 2]"] + class TestExtractJsonTolerantFallback: """extract_json — json_repair fallback for malformed non-OpenAI output.""" @@ -215,3 +219,7 @@ class TestExtractJsonArray: def test_nested_object_array_not_confused_with_array_answer(self) -> None: text = '{"items": [1, 2, 3]}' assert extract_json_array(text) is None + + def test_unmatched_prose_brace_before_valid_array(self) -> None: + text = "Explanation uses {placeholder, final answer [1, 2]" + assert extract_json_array(text) == [1, 2]