"""Pydantic coercion helpers for tool input schemas. LLMs stringify everything — they send `"95727067"` when your schema expects `int`, or `'[{"id":1}]'` when you expect `list[dict]`. Pydantic strict mode returns 422 and the agent fails. Use these as `mode="before"` validators on every tool input schema. Example: from pydantic import BaseModel, field_validator from wxo.tools.python._coercion_helpers import _coerce_int, _coerce_list class ResetPasswordInput(BaseModel): user_id: int groups: list[str] _coerce_user_id = field_validator("user_id", mode="before")(_coerce_int) _coerce_groups = field_validator("groups", mode="before")(_coerce_list) See `docs/known-issues.md#i-003` for the full story. """ from __future__ import annotations import json from typing import Any def _coerce_int(v: Any) -> Any: """Accepts int or stringified int.""" if isinstance(v, str): s = v.strip() if s.lstrip("-").isdigit(): return int(s) return v def _coerce_float(v: Any) -> Any: if isinstance(v, str): try: return float(v.strip()) except ValueError: pass return v def _coerce_bool(v: Any) -> Any: """Accepts bool, 'true'/'false', '1'/'0', 'yes'/'no'.""" if isinstance(v, bool): return v if isinstance(v, str): s = v.strip().lower() if s in ("true", "1", "yes", "y"): return True if s in ("false", "0", "no", "n"): return False return v def _coerce_list(v: Any) -> Any: """Accepts list, stringified JSON list, or single value (wrapped).""" if isinstance(v, list): return v if isinstance(v, str): s = v.strip() if s.startswith("[") and s.endswith("]"): try: parsed = json.loads(s) if isinstance(parsed, list): return parsed except json.JSONDecodeError: pass # Fallback: wrap single value return [v] return v def _coerce_dict(v: Any) -> Any: """Accepts dict or stringified JSON dict.""" if isinstance(v, dict): return v if isinstance(v, str): s = v.strip() if s.startswith("{") and s.endswith("}"): try: parsed = json.loads(s) if isinstance(parsed, dict): return parsed except json.JSONDecodeError: pass return v