mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-20 00:22:31 +00:00
Refactor Tool methods and type handling; introduce JSON Schema support for tool parameters (schema module, validation tests). Made-with: Cursor
233 lines
7.5 KiB
Python
233 lines
7.5 KiB
Python
"""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: optional placeholder int (legacy ctor signature), description, and bounds."""
|
|
|
|
def __init__(
|
|
self,
|
|
value: int = 0,
|
|
*,
|
|
description: str = "",
|
|
minimum: int | None = None,
|
|
maximum: int | None = None,
|
|
enum: tuple[int, ...] | list[int] | None = None,
|
|
nullable: bool = False,
|
|
) -> None:
|
|
self._value = value
|
|
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,
|
|
value: float = 0.0,
|
|
*,
|
|
description: str = "",
|
|
minimum: float | None = None,
|
|
maximum: float | None = None,
|
|
enum: tuple[float, ...] | list[float] | None = None,
|
|
nullable: bool = False,
|
|
) -> None:
|
|
self._value = value
|
|
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 = "",
|
|
**properties: Any,
|
|
) -> dict[str, Any]:
|
|
"""Build root tool parameters ``{"type": "object", "properties": ...}`` for :meth:`Tool.parameters`."""
|
|
return ObjectSchema(
|
|
required=required,
|
|
description=description,
|
|
**properties,
|
|
).to_json_schema()
|