""" LLM JSON Response Parser — 共用 helper (Gap 3 Review P1 優化) ============================================================== 4 個 LLM scanner 原本各自重複 3-path JSON parse 邏輯 (~80 行 × 4 = 320 行). 抽成統一 helper,未來擴加 LLM 的 service 直接呼叫. Origin (2026-04-19 pr-review-toolkit code review 指出): - hermes_rule_quality_job:167-186 - capacity_forecaster_job:204-221 - compliance_scanner_job:446-463 - coverage_evaluator_job:241-257 全部重複 strip ``` → direct parse → NemoTron wrapper → description 內嵌 JSON 的 pattern. 2026-04-19 ogt + Claude Opus 4.7 (1M context) Asia/Taipei """ from __future__ import annotations import json as _json from typing import Any import structlog logger = structlog.get_logger(__name__) def parse_llm_json_response( text: str, required_key: str, logger_context: str = "", ) -> dict[str, Any] | None: """ 解析 LLM JSON 回應,含 3-path fallback. Args: text: LLM 回傳的原始文字 required_key: JSON 必須含的 key (e.g. 'posture_grade', 'priority_actions') 用來區分「真 JSON」vs 「NemoTron wrapper」 logger_context: log warning 時附加的 context (e.g. 'compliance_scan') Returns: dict | None — 解析成功且含 required_key 回 dict,否則 None 3-path fallback: Path 1: 直接 JSON → 有 required_key 就用 Path 2: NemoTron wrapper (description/action_title/reasoning 內嵌 JSON 字串) Path 3: Path 2 的 description 含 required_key 直接採用 失敗時 log warning 但不 raise,呼叫者決定如何 fallback. """ if not text: return None _raw = text.strip() # 剝去 markdown code fence ```json...``` 或 ```...``` if _raw.startswith("```"): _raw = _raw.strip("`").lstrip("json").strip() try: parsed = _json.loads(_raw) except (_json.JSONDecodeError, ValueError) as e: logger.warning( "llm_json_parse_raw_failed", context=logger_context, error=str(e), raw_prefix=_raw[:200], ) return None if not isinstance(parsed, dict): return None # Path 1: 直接 JSON 含 required_key if required_key in parsed: return parsed # Path 2+3: NemoTron wrapper — description / action_title / reasoning 可能是內嵌 JSON for wrapper_key in ("description", "action_title", "reasoning"): desc = parsed.get(wrapper_key) if not desc: continue desc_str = str(desc).strip() # Path 2: description 是內嵌 JSON 字串 if desc_str.startswith("{"): try: inner = _json.loads(desc_str) if isinstance(inner, dict) and required_key in inner: return inner except (_json.JSONDecodeError, ValueError): pass # Path 3: 所有嘗試都失敗 logger.warning( "llm_json_missing_required_key", context=logger_context, required_key=required_key, keys_found=list(parsed.keys())[:10], ) return None