From 5271d529d6a790a93354d7bd89646cfaa9dd207f Mon Sep 17 00:00:00 2001 From: nubs Date: Fri, 5 Jun 2026 07:00:59 +0000 Subject: [PATCH] fix(tool-schemas): preserve web_search time_filter through native tool-call conversion (#2757) --- src/tool_schemas.py | 6 +++ tests/test_web_search_time_filter.py | 60 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/test_web_search_time_filter.py diff --git a/src/tool_schemas.py b/src/tool_schemas.py index d315111..10874ae 100644 --- a/src/tool_schemas.py +++ b/src/tool_schemas.py @@ -1176,6 +1176,12 @@ def function_call_to_tool_block(name: str, arguments: str) -> Optional[ToolBlock content = str(queries) else: content = args.get("query", "") + # Preserve the model-requested freshness filter — the web_search schema + # advertises time_filter and the executor parses {"query","time_filter"}, + # but a bare query string dropped it. Mirrors the read_file JSON idiom. + tf = args.get("time_filter") + if content and isinstance(tf, str) and tf in ("day", "week", "month", "year"): + content = json.dumps({"query": content, "time_filter": tf}) elif tool_type == "read_file": # Plain path (back-compat) unless a line range is requested → JSON. if args.get("offset") or args.get("limit"): diff --git a/tests/test_web_search_time_filter.py b/tests/test_web_search_time_filter.py new file mode 100644 index 0000000..26c489f --- /dev/null +++ b/tests/test_web_search_time_filter.py @@ -0,0 +1,60 @@ +"""Issue #2756 — a native web_search function call must preserve time_filter. + +The web_search schema advertises a time_filter enum and the executor honors it +when content is JSON {"query","time_filter"}, but function_call_to_tool_block's +web_search branch emitted a bare query string and dropped time_filter. These pin +that a valid filter is passed through as JSON, while plain/invalid cases stay a +bare string (back-compat). +""" +import sys +from unittest.mock import MagicMock + +# Clean up any mocks from previous tests to ensure we load real modules. +for mod in ['src.agent_tools', 'src.tool_parsing', 'src.tool_schemas', 'src.tool_execution']: + sys.modules.pop(mod, None) + +# Mock heavy database/model dependencies before importing (avoids the +# src.tool_schemas <-> src.agent_tools circular import pulling in the DB layer). +for mod in [ + 'sqlalchemy', 'sqlalchemy.orm', 'sqlalchemy.ext', 'sqlalchemy.ext.declarative', + 'sqlalchemy.ext.hybrid', 'sqlalchemy.sql', 'sqlalchemy.sql.expression', + 'src.database', 'core.models', 'core.database', 'core.auth' +]: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() + +import json # noqa: E402 + +import src.agent_tools # noqa: E402, F401 +from src.tool_schemas import function_call_to_tool_block # noqa: E402 + + +def test_time_filter_is_preserved_as_json(): + block = function_call_to_tool_block( + "web_search", json.dumps({"query": "openai pricing", "time_filter": "year"}) + ) + assert block is not None and block.tool_type == "web_search" + parsed = json.loads(block.content) + assert parsed["query"] == "openai pricing" + assert parsed["time_filter"] == "year" + + +def test_plain_query_stays_bare_string(): + block = function_call_to_tool_block("web_search", json.dumps({"query": "openai pricing"})) + assert block.content == "openai pricing" + + +def test_invalid_time_filter_falls_back_to_bare_query(): + block = function_call_to_tool_block( + "web_search", json.dumps({"query": "openai pricing", "time_filter": "decade"}) + ) + assert block.content == "openai pricing" + + +def test_queries_list_shape_still_carries_filter(): + block = function_call_to_tool_block( + "web_search", json.dumps({"queries": ["latest gpu prices"], "time_filter": "week"}) + ) + parsed = json.loads(block.content) + assert parsed["query"] == "latest gpu prices" + assert parsed["time_filter"] == "week"