Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions astrbot/core/agent/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 77 additions & 0 deletions tests/unit/test_tool_google_schema.py
Original file line number Diff line number Diff line change
@@ -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"}
Loading