feat: AIPlanner JSON 파싱 로직 개선 및 다중 JSON 객체 처리 추가

- `_parse_json()` 메서드에서 응답 내 여러 JSON 객체 중 `actions`를 포함한 계획 객체를 우선 선택하도록 로직 개선
- JSON 파싱 실패 시 잘림 복구 로직을 추가하여 안정성 향상
- 관련 단위 테스트 추가 및 README.md에 변경 사항 반영
This commit is contained in:
kswdev0
2026-03-26 11:36:45 +09:00
parent db08db62a3
commit ace5d63480
3 changed files with 54 additions and 47 deletions

View File

@@ -451,55 +451,46 @@ class AIPlanner:
+ "\n"
+ raw[:300]
)
brace_depth = 0
bracket_depth = 0
in_string = False
escape = False
end = start
for i in range(start, len(text)):
c = text[i]
if escape:
escape = False
continue
if c == '\\' and in_string:
escape = True
continue
if c == '"' and not escape:
in_string = not in_string
continue
if in_string:
continue
if c == '{':
brace_depth += 1
elif c == '}':
brace_depth -= 1
elif c == '[':
bracket_depth += 1
elif c == ']':
bracket_depth -= 1
# 응답 안에 { ... }가 여러 개(예: 인벤토리 하위 객체, 분석용 JSON)가 섞일 수 있다.
# decide() 파이프라인은 "actions" 스키마를 가진 계획 객체가 필요하므로,
# candidates 중 actions를 포함하는 것을 우선 선택한다.
search_pos = start
for _ in range(10):
next_start = text.find("{", search_pos)
if next_start == -1:
break
candidate = self._extract_balanced_json_candidate(text, next_start)
if candidate:
try:
loaded = json.loads(candidate)
if (
isinstance(loaded, dict)
and isinstance(loaded.get("actions"), list)
):
return loaded
except json.JSONDecodeError:
# candidate가 중간에 잘려 실패한 경우 다음 '{'를 시도
pass
search_pos = next_start + 1
if brace_depth == 0 and bracket_depth == 0:
# 최상위 JSON 객체가 종료된 지점으로 추정
if i > start:
end = i + 1
break
if brace_depth != 0 or bracket_depth != 0:
partial = text[start:]
partial = self._repair_truncated_json(partial)
try:
return json.loads(partial)
except json.JSONDecodeError:
raise ValueError(f"JSON 파싱 실패 (잘린 응답 복구 불가):\n{raw[:400]}")
candidate = text[start:end]
# 그래도 actions 스키마를 찾지 못하면, 잘림 복구 로직으로 마지막 수습을 시도한다.
partial = text[start:]
repaired = self._repair_truncated_json(partial)
try:
return json.loads(candidate)
loaded = json.loads(repaired)
if isinstance(loaded, dict):
return loaded
except json.JSONDecodeError:
# 중괄호는 맞지만 배열/후행 속성이 잘려 파싱 실패하는 케이스 복구
repaired = self._repair_truncated_json(candidate)
try:
return json.loads(repaired)
except json.JSONDecodeError:
raise ValueError(f"JSON 파싱 실패:\n{raw[:400]}")
pass
first_non_ws = next((c for c in text if not c.isspace()), "")
raise ValueError(
"JSON 파싱 실패: actions 스키마를 포함한 후보를 찾지 못함 "
"(first_non_ws="
+ repr(first_non_ws)
+ ")\n"
+ raw[:400]
)
@staticmethod
def _extract_balanced_json_candidate(text: str, start: int) -> str: