fix(ai): parse_json_response — raw newline escape fallback (5단계)
Phase 4-A debug 결과 study_question_jobs.parse_fail 33건의 raw preview 분석:
- 모델이 explanation_md 안에 raw newline (LF) 그대로 박음 ('### [풀이]\n\n**자료...')
- JSON 표준상 string literal 안 raw control char 금지 → json.loads 거부
- 4단계 fallback (greedy slice) 도 이 때문에 실패
5단계 fallback 추가: candidate 의 \r\n/\n/\r 을 ``\\n``/``\\r`` escape 로 치환 후 재시도.
이미 escape 된 ``\\n`` (Python str = backslash+n 두 글자) 는 raw newline 아니라 영향 없음.
다른 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:
+18
-8
@@ -54,14 +54,24 @@ def parse_json_response(raw: str) -> dict | None:
|
||||
# 4. greedy slice fallback — first '{' ~ last '}' 까지
|
||||
first = cleaned.find("{")
|
||||
last = cleaned.rfind("}")
|
||||
if first >= 0 and last > first:
|
||||
candidate = cleaned[first : last + 1]
|
||||
try:
|
||||
obj = json.loads(candidate)
|
||||
return obj if isinstance(obj, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
if first < 0 or last <= first:
|
||||
return None
|
||||
candidate = cleaned[first : last + 1]
|
||||
try:
|
||||
obj = json.loads(candidate)
|
||||
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")
|
||||
try:
|
||||
obj = json.loads(escaped)
|
||||
return obj if isinstance(obj, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
# 프롬프트 로딩
|
||||
PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
||||
|
||||
Reference in New Issue
Block a user