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:
+66
-5
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user