From 92597c119debb1fea4040f11493ef7b07e21b2b1 Mon Sep 17 00:00:00 2001 From: Kalin Ovtcharov Date: Fri, 2 Oct 2026 00:41:21 -0700 Subject: [PATCH] feat(tools): a Literal-typed tool argument is sent as a JSON-schema enum Asked to look up a GitHub release, local Gemma called fetch_page with extract=True twice and got "Invalid extract mode 'True'" both times: the schema said only "string". Literal[...] annotations now add an enum to the tool schema, so the server's tool-call grammar can rule such values out; fetch_page's extract is the first to use it. --- src/gaia/agents/base/agent.py | 2 ++ src/gaia/agents/base/tools.py | 16 +++++++++ src/gaia/agents/tools/browser_tools.py | 3 +- tests/unit/test_tool_enum_args.py | 45 ++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_tool_enum_args.py diff --git a/src/gaia/agents/base/agent.py b/src/gaia/agents/base/agent.py index 6411bf6c9c..0ad3279362 100644 --- a/src/gaia/agents/base/agent.py +++ b/src/gaia/agents/base/agent.py @@ -4154,6 +4154,8 @@ def _python_to_json_type(py_type: str) -> str: desc = param_info.get("description", "") if desc: prop["description"] = desc + if param_info.get("enum"): + prop["enum"] = list(param_info["enum"]) properties[param_name] = prop if param_info.get("required", True): required.append(param_name) diff --git a/src/gaia/agents/base/tools.py b/src/gaia/agents/base/tools.py index 41fa9418c9..2b4534739c 100644 --- a/src/gaia/agents/base/tools.py +++ b/src/gaia/agents/base/tools.py @@ -117,6 +117,17 @@ def _infer_param_type(annotation: Any) -> str: return "unknown" +def _literal_choices(annotation: Any) -> Optional[list]: + """The values of a ``Literal[...]`` (or ``Optional[Literal[...]]``), else None.""" + origin = typing.get_origin(annotation) + if origin is typing.Union or origin is types.UnionType: + non_none = [a for a in typing.get_args(annotation) if a is not types.NoneType] + return _literal_choices(non_none[0]) if len(non_none) == 1 else None + if origin is typing.Literal: + return list(typing.get_args(annotation)) + return None + + def _parse_arg_descriptions(docstring: Optional[str]) -> Dict[str, str]: """Extract per-argument text from a Google-style ``Args:`` block. @@ -266,6 +277,11 @@ def decorator(f: Callable) -> Callable: "type": _infer_param_type(annotation), "required": param.default == inspect.Parameter.empty, } + choices = _literal_choices(annotation) + if choices: + # An enum lets the server's tool-call grammar rule out bad values. + param_info["type"] = _infer_param_type(type(choices[0])) + param_info["enum"] = choices description = arg_descriptions.get(name, "").strip() if description: diff --git a/src/gaia/agents/tools/browser_tools.py b/src/gaia/agents/tools/browser_tools.py index 2b5f884823..ea7b4ae06f 100644 --- a/src/gaia/agents/tools/browser_tools.py +++ b/src/gaia/agents/tools/browser_tools.py @@ -13,6 +13,7 @@ import json import logging from pathlib import Path +from typing import Literal logger = logging.getLogger(__name__) @@ -46,7 +47,7 @@ def _ensure_web_client() -> bool: @tool(atomic=True) def fetch_page( url: str, - extract: str = "text", + extract: Literal["text", "html", "links", "tables"] = "text", max_length: int = 5000, ) -> str: """Fetch a web page and extract its content. diff --git a/tests/unit/test_tool_enum_args.py b/tests/unit/test_tool_enum_args.py new file mode 100644 index 0000000000..5cbafbe682 --- /dev/null +++ b/tests/unit/test_tool_enum_args.py @@ -0,0 +1,45 @@ +"""A Literal-typed tool argument reaches the model as a JSON-schema enum.""" + +from types import SimpleNamespace +from typing import Literal, Optional + +from gaia.agents.base.tools import tool + + +def test_literal_becomes_enum_in_the_registry(): + registry = {} + + @tool(registry=registry) + def pick(mode: Literal["text", "html"] = "text", other: Optional[str] = None): + """Pick. + + Args: + mode: Which. + other: Something else. + """ + + params = registry["pick"]["parameters"] + assert params["mode"]["enum"] == ["text", "html"] + assert params["mode"]["type"] == "string" + assert "enum" not in params["other"] + + +def test_fetch_page_schema_lists_its_extract_modes(): + from gaia.agents.base.agent import Agent + from gaia.agents.base.tools import _TOOL_REGISTRY + from gaia.agents.tools.browser_tools import BrowserToolsMixin + + class Probe(BrowserToolsMixin): + _web_client = None + + Probe().register_browser_tools() + agent = SimpleNamespace( + _tools_registry={"fetch_page": _TOOL_REGISTRY["fetch_page"]} + ) + (schema,) = Agent._build_openai_tool_schemas(agent) + extract = schema["function"]["parameters"]["properties"]["extract"] + assert extract == { + "type": "string", + "description": extract["description"], + "enum": ["text", "html", "links", "tables"], + }