From ea882213b7a5756ee72ae4decddccfc2f602c01a Mon Sep 17 00:00:00 2001 From: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:13:03 -0700 Subject: [PATCH] fix: ensure Gemini array schemas always include items --- astrbot/core/agent/tool.py | 11 +++- tests/unit/test_tool_google_schema.py | 77 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_tool_google_schema.py diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index c2536708e6..a7b5944317 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -293,8 +293,15 @@ def convert_schema(schema: dict) -> dict: if properties: result["properties"] = properties - if "items" in schema: - result["items"] = convert_schema(schema["items"]) + if target_type == "array": + items_schema = schema.get("items") + if isinstance(items_schema, dict): + result["items"] = convert_schema(items_schema) + else: + # Gemini requires array schemas to include an `items` schema. + # JSON Schema allows omitting it, so fall back to a permissive + # string item schema instead of emitting an invalid declaration. + result["items"] = {"type": "string"} return result diff --git a/tests/unit/test_tool_google_schema.py b/tests/unit/test_tool_google_schema.py new file mode 100644 index 0000000000..f1046e6af3 --- /dev/null +++ b/tests/unit/test_tool_google_schema.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import importlib.util +import sys +import types +from pathlib import Path +from typing import Generic, TypeVar + +REPO_ROOT = Path(__file__).resolve().parents[2] +TOOL_MODULE_PATH = REPO_ROOT / "astrbot/core/agent/tool.py" + + +def load_tool_module(): + package_names = [ + "astrbot", + "astrbot.core", + "astrbot.core.agent", + "astrbot.core.message", + ] + for name in package_names: + if name not in sys.modules: + module = types.ModuleType(name) + module.__path__ = [] + sys.modules[name] = module + + message_result_module = types.ModuleType( + "astrbot.core.message.message_event_result" + ) + message_result_module.MessageEventResult = type("MessageEventResult", (), {}) + sys.modules[message_result_module.__name__] = message_result_module + + run_context_module = types.ModuleType("astrbot.core.agent.run_context") + run_context_module.TContext = TypeVar("TContext") + + class ContextWrapper(Generic[run_context_module.TContext]): + pass + + run_context_module.ContextWrapper = ContextWrapper + sys.modules[run_context_module.__name__] = run_context_module + + spec = importlib.util.spec_from_file_location( + "astrbot.core.agent.tool", TOOL_MODULE_PATH + ) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_google_schema_fills_missing_array_items_with_string_schema(): + tool_module = load_tool_module() + FunctionTool = tool_module.FunctionTool + ToolSet = tool_module.ToolSet + + tool = FunctionTool( + name="search_sources", + description="Search sources by UUID.", + parameters={ + "type": "object", + "properties": { + "source_uuids": { + "type": "array", + "description": "Optional list of source UUIDs.", + } + }, + "required": ["source_uuids"], + }, + ) + + schema = ToolSet([tool]).google_schema() + source_uuids = schema["function_declarations"][0]["parameters"]["properties"][ + "source_uuids" + ] + + assert source_uuids["type"] == "array" + assert source_uuids["items"] == {"type": "string"}