|
| 1 | +"""JSON parse check evaluator. |
| 2 | +
|
| 3 | +Tries to parse final_response as JSON. Optionally extracts fenced ```json ... ``` blocks. |
| 4 | +
|
| 5 | +Config: |
| 6 | + require_json (bool, default False): If False, evaluator is a no-op (1.0). |
| 7 | + extract_markdown_fence (bool, default True): Strip ```json fences if present. |
| 8 | +
|
| 9 | +Usage: |
| 10 | + config: |
| 11 | + require_json: true |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import json |
| 17 | +import re |
| 18 | + |
| 19 | +from agentevals_evaluator_sdk import EvalInput, EvalResult, evaluator |
| 20 | + |
| 21 | +_FENCE = re.compile(r"^```(?:json)?\s*\n?(.*?)\n?```\s*$", re.DOTALL | re.IGNORECASE) |
| 22 | + |
| 23 | + |
| 24 | +def _parse_json_payload(text: str, extract_fence: bool) -> object: |
| 25 | + raw = (text or "").strip() |
| 26 | + if extract_fence: |
| 27 | + m = _FENCE.match(raw) |
| 28 | + if m: |
| 29 | + raw = m.group(1).strip() |
| 30 | + return json.loads(raw) |
| 31 | + |
| 32 | + |
| 33 | +@evaluator |
| 34 | +def is_json(input: EvalInput) -> EvalResult: |
| 35 | + if not input.config.get("require_json"): |
| 36 | + return EvalResult( |
| 37 | + score=1.0, |
| 38 | + per_invocation_scores=[1.0] * len(input.invocations), |
| 39 | + details={"note": "require_json not set; skipping check"}, |
| 40 | + ) |
| 41 | + |
| 42 | + extract_fence = bool(input.config.get("extract_markdown_fence", True)) |
| 43 | + |
| 44 | + scores: list[float] = [] |
| 45 | + issues: list[str] = [] |
| 46 | + |
| 47 | + for inv in input.invocations: |
| 48 | + try: |
| 49 | + _parse_json_payload(inv.final_response or "", extract_fence) |
| 50 | + scores.append(1.0) |
| 51 | + except (json.JSONDecodeError, TypeError, ValueError) as exc: |
| 52 | + scores.append(0.0) |
| 53 | + issues.append(f"{inv.invocation_id}: not valid JSON ({exc})") |
| 54 | + |
| 55 | + overall = sum(scores) / len(scores) if scores else 0.0 |
| 56 | + return EvalResult( |
| 57 | + score=overall, |
| 58 | + per_invocation_scores=scores, |
| 59 | + details={"issues": issues} if issues else None, |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +if __name__ == "__main__": |
| 64 | + is_json.run() |
0 commit comments