diff --git a/app/ai/client.py b/app/ai/client.py index 6a126ab..282a7a8 100644 --- a/app/ai/client.py +++ b/app/ai/client.py @@ -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"