from __future__ import annotations import ast import json from collections.abc import Mapping, Sequence from dataclasses import asdict, is_dataclass from typing import Any def to_jsonable(value: Any) -> Any: """Return plain JSON-compatible Python values. Gradio and Pydantic can wrap JSON payloads in RootModel-like objects. Saved evaluation reports should have exactly the same shape whether they are fresh, restored from disk, returned through FastAPI, or rendered in the UI. """ if value is None or isinstance(value, (str, int, float, bool)): if isinstance(value, str) and value.startswith("root="): try: return to_jsonable(ast.literal_eval(value[5:])) except Exception: return value return value if hasattr(value, "model_dump"): try: dumped = value.model_dump(mode="json") except TypeError: dumped = value.model_dump() return to_jsonable(dumped) if is_dataclass(value): return to_jsonable(asdict(value)) if isinstance(value, Mapping): plain = {str(key): to_jsonable(item) for key, item in value.items()} if set(plain) == {"root"} and isinstance(plain["root"], (dict, list)): return plain["root"] return plain if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return [to_jsonable(item) for item in value] item = getattr(value, "item", None) if callable(item): try: return to_jsonable(item()) except Exception: pass return str(value) def pretty_json(value: Any) -> str: return json.dumps(to_jsonable(value), indent=2, ensure_ascii=False)