"""JSON Schema fragment types: all subclass :class:`~nanobot.agent.tools.base.Schema` for descriptions and constraints on tool parameters. - ``to_json_schema()``: returns a dict compatible with :meth:`~nanobot.agent.tools.base.Schema.validate_json_schema_value` / :class:`~nanobot.agent.tools.base.Tool`. - ``validate_value(value, path)``: validates a single value against this schema; returns a list of error messages (empty means valid). Shared validation and fragment normalization are on the class methods of :class:`~nanobot.agent.tools.base.Schema`. Note: Python does not allow subclassing ``bool``, so booleans use :class:`BooleanSchema`. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from nanobot.agent.tools.base import Schema class StringSchema(Schema): """String parameter: ``description`` documents the field; optional length bounds and enum.""" def __init__( self, description: str = "", *, min_length: int | None = None, max_length: int | None = None, enum: tuple[Any, ...] | list[Any] | None = None, nullable: bool = False, ) -> None: self._description = description self._min_length = min_length self._max_length = max_length self._enum = tuple(enum) if enum is not None else None self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "string" if self._nullable: t = ["string", "null"] d: dict[str, Any] = {"type": t} if self._description: d["description"] = self._description if self._min_length is not None: d["minLength"] = self._min_length if self._max_length is not None: d["maxLength"] = self._max_length if self._enum is not None: d["enum"] = list(self._enum) return d class IntegerSchema(Schema): """Integer parameter with a description and optional bounds.""" def __init__( self, *, description: str = "", minimum: int | None = None, maximum: int | None = None, enum: tuple[int, ...] | list[int] | None = None, nullable: bool = False, ) -> None: self._description = description self._minimum = minimum self._maximum = maximum self._enum = tuple(enum) if enum is not None else None self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "integer" if self._nullable: t = ["integer", "null"] d: dict[str, Any] = {"type": t} if self._description: d["description"] = self._description if self._minimum is not None: d["minimum"] = self._minimum if self._maximum is not None: d["maximum"] = self._maximum if self._enum is not None: d["enum"] = list(self._enum) return d class NumberSchema(Schema): """Numeric parameter (JSON number): description and optional bounds.""" def __init__( self, *, description: str = "", minimum: float | None = None, maximum: float | None = None, enum: tuple[float, ...] | list[float] | None = None, nullable: bool = False, ) -> None: self._description = description self._minimum = minimum self._maximum = maximum self._enum = tuple(enum) if enum is not None else None self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "number" if self._nullable: t = ["number", "null"] d: dict[str, Any] = {"type": t} if self._description: d["description"] = self._description if self._minimum is not None: d["minimum"] = self._minimum if self._maximum is not None: d["maximum"] = self._maximum if self._enum is not None: d["enum"] = list(self._enum) return d class BooleanSchema(Schema): """Boolean parameter (standalone class because Python forbids subclassing ``bool``).""" def __init__( self, *, description: str = "", default: bool | None = None, nullable: bool = False, ) -> None: self._description = description self._default = default self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "boolean" if self._nullable: t = ["boolean", "null"] d: dict[str, Any] = {"type": t} if self._description: d["description"] = self._description if self._default is not None: d["default"] = self._default return d class ArraySchema(Schema): """Array parameter: element schema is given by ``items``.""" def __init__( self, items: Any | None = None, *, description: str = "", min_items: int | None = None, max_items: int | None = None, nullable: bool = False, ) -> None: self._items_schema: Any = items if items is not None else StringSchema("") self._description = description self._min_items = min_items self._max_items = max_items self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "array" if self._nullable: t = ["array", "null"] d: dict[str, Any] = { "type": t, "items": Schema.fragment(self._items_schema), } if self._description: d["description"] = self._description if self._min_items is not None: d["minItems"] = self._min_items if self._max_items is not None: d["maxItems"] = self._max_items return d class ObjectSchema(Schema): """Object parameter: ``properties`` or keyword args are field names; values are child Schema or JSON Schema dicts.""" def __init__( self, properties: Mapping[str, Any] | None = None, *, required: list[str] | None = None, description: str = "", additional_properties: bool | dict[str, Any] | None = None, nullable: bool = False, **kwargs: Any, ) -> None: self._properties = dict(properties or {}, **kwargs) self._required = list(required or []) self._root_description = description self._additional_properties = additional_properties self._nullable = nullable def to_json_schema(self) -> dict[str, Any]: t: Any = "object" if self._nullable: t = ["object", "null"] props = {k: Schema.fragment(v) for k, v in self._properties.items()} out: dict[str, Any] = {"type": t, "properties": props} if self._required: out["required"] = self._required if self._root_description: out["description"] = self._root_description if self._additional_properties is not None: out["additionalProperties"] = self._additional_properties return out def tool_parameters_schema( *, required: list[str] | None = None, description: str = "", additional_properties: bool | dict[str, Any] | None = False, **properties: Any, ) -> dict[str, Any]: """Build root tool parameters ``{"type": "object", "properties": ...}`` for :meth:`Tool.parameters`. Built-in tools default to strict parameter objects so misspelled tool-call arguments are reported before execution instead of being silently ignored. Pass ``additional_properties=None`` to omit the JSON Schema keyword. """ return ObjectSchema( required=required, description=description, additional_properties=additional_properties, **properties, ).to_json_schema()