fix(ai): parse_json_response — string literal 안만 fix 하는 stateful walker

직전 fallback 의 무차별 newline replace 가 string 외부 (object 구조) 의 raw newline
까지 escape 해서 JSON 거부. 또 LaTeX 수식 (\circ, \text, \, etc) 의 invalid backslash
는 newline 이슈와 별개라 별도 fix 필요.

state machine: in_string 토글 (`\"` 만남). string literal 안에서만:
- raw LF/CR/TAB → \\n/\\r/\\t 로 변환
- backslash 다음에 valid escape char (\"\\/bfnrtu) 면 그대로
- backslash 다음에 invalid (\\c, \\,) 면 backslash 자체를 \\\\ 로 escape
- string 외부 raw newline 은 JSON whitespace 라 보존

운영 데이터 id=243 의 raw 940자에 \\circ \\text \\, \\approx \\times 등 다수 LaTeX +
markdown 줄바꿈 → 새 walker 가 두 케이스 모두 fix. 다른 worker (classify/triage/
study_explanation/evidence/study_session_analysis) 자동 혜택.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-05-02 08:00:20 +09:00
parent 95b127fd8d
commit b3dbf1a11e
+66 -5
View File
@@ -62,17 +62,78 @@ def parse_json_response(raw: str) -> dict | None:
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
pass
# 5. (Phase 4-A 후속) Markdown 줄바꿈이 JSON string literal 안에 raw 로 들어간
# 케이스 방어. JSON 표준은 string 안 raw newline 금지. raw \n / \r 을 \\n/\\r 로
# escape 후 재시도. 이미 escape 된 ``\\n`` (Python str 로는 backslash+n 두 글자)
# 는 raw newline 아니라 영향 없음.
escaped = candidate.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\r")
# 5. (Phase 4-A 후속) Markdown 줄바꿈 + LaTeX 수식이 JSON string literal 안에
# raw 로 들어간 케이스 방어. 두 가지 invalid:
# - raw newline (LF/CR/TAB) — JSON 표준 string 안 control char 금지
# - invalid backslash — `\circ`, `\text`, `\,` 같은 LaTeX. JSON valid escape
# 은 `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX` 만.
# stateful walker — string literal 안에서만 fix. 외부 (object 구조) 의 newline
# 은 valid whitespace 라 보존.
escaped = _fix_json_string_escapes(candidate)
try:
obj = json.loads(escaped)
return obj if isinstance(obj, dict) else None
except json.JSONDecodeError:
return None
_VALID_JSON_ESCAPES = set('"\\/bfnrtu')
def _fix_json_string_escapes(s: str) -> str:
"""JSON string literal 안의 raw newline + invalid backslash 만 escape.
state machine: in_string 토글 (`"` 마주침). string 안에서만:
- raw LF/CR/TAB → ``\\n``/``\\r``/``\\t`` 로 변환
- 백슬래시 다음에 valid escape char (`"\\/bfnrtu`) 면 그대로
- 백슬래시 다음에 invalid char (`\\c`, `\\,`) 면 백슬래시 자체를 ``\\\\`` 로 escape
string 외부 (`{` `,` `:` 사이) 의 raw newline 등은 JSON whitespace 라 보존.
"""
out: list[str] = []
i = 0
n = len(s)
in_string = False
while i < n:
ch = s[i]
if not in_string:
if ch == '"':
in_string = True
out.append(ch)
i += 1
continue
# in_string
if ch == "\\":
nxt = s[i + 1] if i + 1 < n else ""
if nxt in _VALID_JSON_ESCAPES:
out.append(ch)
out.append(nxt)
i += 2
continue
# invalid escape — backslash 자체를 escape
out.append("\\\\")
i += 1
continue
if ch == '"':
in_string = False
out.append(ch)
i += 1
continue
if ch == "\n":
out.append("\\n")
i += 1
continue
if ch == "\r":
out.append("\\r")
i += 1
continue
if ch == "\t":
out.append("\\t")
i += 1
continue
out.append(ch)
i += 1
return "".join(out)
# 프롬프트 로딩
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"