diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 2cf6b6747c0..9e31fcc60db 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -1,4 +1,5 @@ import copy +import json from collections.abc import AsyncGenerator, Awaitable, Callable from typing import Any, Generic @@ -303,6 +304,25 @@ def convert_schema(schema: dict) -> dict: } result.update({k: schema[k] for k in support_fields if k in schema}) + # Gemini only accepts string enums ("enum: only allowed for STRING + # type"). Other fields keep their type, so the model still sends + # e.g. an integer, and list the allowed values in the description. + if "enum" in result: + enum = result.pop("enum") + values = [v for v in enum if v is not None] + if None in enum: + result["nullable"] = True + if result["type"] == "string": + result["enum"] = [ + v if isinstance(v, str) else json.dumps(v) for v in values + ] + else: + allowed = ", ".join(json.dumps(v) for v in values) + description = result.get("description", "") + result["description"] = ( + f"{description}\nAllowed values: {allowed}".strip() + ) + if "properties" in schema: properties = {} for key, value in schema["properties"].items(): diff --git a/tests/unit/test_tool_google_schema.py b/tests/unit/test_tool_google_schema.py index 0826e99e77d..6385778ec5a 100644 --- a/tests/unit/test_tool_google_schema.py +++ b/tests/unit/test_tool_google_schema.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Generic, TypeVar +from google.genai import types as genai_types + REPO_ROOT = Path(__file__).resolve().parents[2] TOOL_MODULE_PATH = REPO_ROOT / "astrbot/core/agent/tool.py" @@ -77,6 +79,49 @@ def test_google_schema_fills_missing_array_items_with_string_schema(): assert source_uuids["items"] == {"type": "string"} +def test_google_schema_keeps_enums_only_on_string_fields(): + tool_module = load_tool_module() + FunctionTool = tool_module.FunctionTool + ToolSet = tool_module.ToolSet + + tool = FunctionTool( + name="list_items", + description="List items.", + parameters={ + "type": "object", + "properties": { + "page_size": { + "type": "integer", + "description": "Items per page.", + "enum": [10, 20, 50], + }, + "strict": {"type": "boolean", "enum": [True]}, + "order": {"type": ["string", "null"], "enum": ["asc", "desc", None]}, + }, + }, + ) + + schema = ToolSet([tool]).google_schema() + properties = schema["function_declarations"][0]["parameters"]["properties"] + + assert properties["page_size"] == { + "type": "integer", + "description": "Items per page.\nAllowed values: 10, 20, 50", + } + assert properties["strict"] == { + "type": "boolean", + "description": "Allowed values: true", + } + assert properties["order"] == { + "type": "string", + "enum": ["asc", "desc"], + "nullable": True, + } + assert tool.parameters["properties"]["page_size"]["enum"] == [10, 20, 50] + # The Gemini SDK rejects non-string enum values when the request is built. + genai_types.Tool(function_declarations=schema["function_declarations"]) + + def test_openai_schema_sorts_tools_by_name_without_mutating_toolset_order(): tool_module = load_tool_module() FunctionTool = tool_module.FunctionTool