"""Coerce tool arguments that some models serialize as JSON strings.""" from __future__ import annotations import json from typing import Any def coerce_json_list(value: Any) -> Any: """Return ``value`` unchanged, or parse it if it's a JSON-list string. ``value`` is returned as-is when it's already a list, isn't a string, doesn't look like JSON, or fails to parse. A bare JSON object is wrapped in a single-element list so the caller sees a uniform shape. """ if not isinstance(value, str): return value s = value.strip() if not (s.startswith("[") or s.startswith("{")): return value try: parsed = json.loads(s) except (json.JSONDecodeError, ValueError): return value if isinstance(parsed, dict): return [parsed] return parsed def coerce_json_object(value: Any) -> dict[str, Any] | None: """Return ``value`` as an object, or ``None`` if it isn't usable. Tolerates a double-encoded JSON-object string (``'{"description": "..."}'``) the same way :func:`coerce_json_list` tolerates a double-encoded list at the top level — some models serialise each list element as its own JSON string. A plain (non-JSON) string is NOT coerced: callers surface it as an error so the model re-sends the documented shape rather than silently losing the item. """ if isinstance(value, dict): return value if isinstance(value, str): parsed = coerce_json_list(value) if isinstance(parsed, list) and len(parsed) == 1 and isinstance(parsed[0], dict): return parsed[0] return None __all__ = ["coerce_json_list", "coerce_json_object"]