From a07fe35936795177eb75f601114256669e70204e Mon Sep 17 00:00:00 2001 From: Dividesbyzer0 <54127744+zoomdbz@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:02:10 -0400 Subject: [PATCH] fix(agent): honor explicit web search requests Promote explicit web-search phrasing to tool use and keep web_search/web_fetch available for that turn even when the stale web toggle is false. --- routes/chat_routes.py | 7 ++++++- src/action_intents.py | 3 +++ src/agent_loop.py | 5 +++-- src/tool_index.py | 4 ++++ tests/test_action_intents.py | 7 +++++++ tests/test_chat_route_tool_policy.py | 21 ++++++++++++++++++++- tests/test_tool_rag_keyword_hints.py | 8 ++++++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/routes/chat_routes.py b/routes/chat_routes.py index 7ad635576..c9164621d 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -696,7 +696,12 @@ def setup_chat_routes( # by default without having to send allow_bash in every request. if allow_bash is not None and str(allow_bash).lower() != "true": disabled_tools.add("bash") - if allow_web_search is not None and str(allow_web_search).lower() != "true": + _explicit_web_intent = bool(_tool_intent and _tool_intent.category == "web") + if ( + allow_web_search is not None + and str(allow_web_search).lower() != "true" + and not _explicit_web_intent + ): disabled_tools.add("web_search") disabled_tools.add("web_fetch") diff --git a/src/action_intents.py b/src/action_intents.py index ea0cbc86d..3b9c3cc73 100644 --- a/src/action_intents.py +++ b/src/action_intents.py @@ -91,6 +91,9 @@ _ROUTING_PATTERNS: tuple[tuple[str, str, Pattern[str]], ...] = tuple( ("ui", "tool or feature toggle request", r"\b(?:disable|enable|turn\s+(?:on|off))\s+(?:the\s+)?(?:shell|search|web|browser|documents?|memory|skills|images?|calendar|email|mail|research|incognito)\b"), # Deep research jobs, not quick conceptual mentions of research. + ("web", "explicit web search request", rf"{_PLEASE}(?:do|run|use|perform|make)\s+(?:a\s+)?(?:web\s+search|search\s+the\s+web)\b.+"), + ("web", "web lookup imperative request", rf"{_PLEASE}(?:web\s+search|search\s+the\s+web|search\s+online|look\s+up|google)\b.+"), + ("web", "assistant web lookup request", rf"{_ACTION_QUESTION}(?:web\s+search|search\s+the\s+web|search\s+online|look\s+up|google)\b.+"), ("research", "deep research imperative request", rf"{_PLEASE}(?:research|deep\s+dive|look\s+into|investigate)\s+.+"), ("research", "assistant deep research request", rf"{_ACTION_QUESTION}(?:research|do\s+research|deep\s+dive|look\s+into|investigate)\s+.+"), diff --git a/src/agent_loop.py b/src/agent_loop.py index 5b9bb2ba9..a4525e93c 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -2099,11 +2099,12 @@ async def stream_agent_loop( # tool, so we don't nudge on harmless transitional text like "let me # know what you think". _INTENT_RE = re.compile( - r"(?:^|\n)\s*(?:let me|i'?ll|i will|going to|let's)\s+" + r"(?:^|\n)\s*(?:let me|i'?ll|i will|i need to|we need to|need to|" + r"i should|we should|i must|we must|going to|let's)\s+" r"(?:tail|check|investigate|look at|see|tail|read|fetch|inspect|" r"verify|diagnose|examine|debug|capture|grab|pull|view|run|call|" r"trigger|launch|start|kick off|stop|kill|restart|adopt|serve|" - r"register|adopt|list|search|find|query|hit|ping|test)" + r"register|adopt|list|search|find|query|hit|ping|test|use|perform|do)" r"\b[^.\n]{0,140}", re.IGNORECASE, ) diff --git a/src/tool_index.py b/src/tool_index.py index 32c7bcf41..a45d3b4a8 100644 --- a/src/tool_index.py +++ b/src/tool_index.py @@ -384,6 +384,10 @@ class ToolIndex: "delegate to", "have model"}): {"chat_with_model", "ask_teacher", "list_models"}, # Deep research intent (incl. common typo "reserach") + frozenset({"web search", "search the web", "search online", "look up", + "google", "latest", "current", "news", "weather", + "forecast", "stock price", "price of"}): + {"web_search", "web_fetch"}, frozenset({"research", "reserach", "reasearch", "look into", "investigate", "deep dive", "deep research", "find out about", "study up on", "report on", "do research", "look up everything"}): diff --git a/tests/test_action_intents.py b/tests/test_action_intents.py index 02b4623eb..f52b408e4 100644 --- a/tests/test_action_intents.py +++ b/tests/test_action_intents.py @@ -49,6 +49,13 @@ def test_research_action_promotes_to_agent(): assert message_needs_tools("can you look into GPU hosting options") +def test_explicit_web_search_promotes_to_agent(): + assert message_needs_tools("use web search and find a recipe for chocolate chip cookies") + assert message_needs_tools("do a web search for the best chocolate chip cookies") + assert message_needs_tools("search the web for current RTX 3090 prices") + assert classify_tool_intent("use web search and find a recipe").category == "web" + + def test_explanatory_calendar_questions_stay_plain_chat(): assert not message_needs_tools("How do I add an entry to my calendar?") assert not message_needs_tools("What about the built-in Odysseus calendar, is that linked to email?") diff --git a/tests/test_chat_route_tool_policy.py b/tests/test_chat_route_tool_policy.py index 21fb78616..869b9a972 100644 --- a/tests/test_chat_route_tool_policy.py +++ b/tests/test_chat_route_tool_policy.py @@ -89,6 +89,9 @@ def test_disabled_tools_does_not_bash_when_allow_bash_is_none(): assert "allow_web_search is not None" in source, ( "disabled_tools check must guard against allow_web_search being None" ) + assert "_explicit_web_intent" in source and "not _explicit_web_intent" in source, ( + "explicit web-search requests must override an off web toggle for that turn" + ) # ── Functional tests of the disabled-tools logic ─────────────── @@ -99,6 +102,7 @@ def _build_disabled_tools( allow_web_search=None, can_use_bash=True, can_use_browser=True, + explicit_web_intent=False, ): """Replicate the disabled-tools logic from chat_stream for unit testing. @@ -109,7 +113,11 @@ def _build_disabled_tools( # Issue #3229 fix: only disable when explicitly set to a falsy value. if allow_bash is not None and str(allow_bash).lower() != "true": disabled_tools.add("bash") - if allow_web_search is not None and str(allow_web_search).lower() != "true": + if ( + allow_web_search is not None + and str(allow_web_search).lower() != "true" + and not explicit_web_intent + ): disabled_tools.add("web_search") disabled_tools.add("web_fetch") @@ -148,6 +156,17 @@ def test_json_body_allow_web_search_false_disables_web(): assert "web_fetch" in disabled +def test_explicit_web_intent_overrides_false_web_toggle_for_turn(): + """A stale/off web toggle must not remove web tools when the message + explicitly asks to use web search.""" + disabled = _build_disabled_tools( + allow_web_search="false", + explicit_web_intent=True, + ) + assert "web_search" not in disabled + assert "web_fetch" not in disabled + + def test_admin_user_gets_bash_enabled_by_default(): """When allow_bash is not set and user has can_use_bash privilege, bash must NOT be disabled. diff --git a/tests/test_tool_rag_keyword_hints.py b/tests/test_tool_rag_keyword_hints.py index 5a6f978d2..5e68eca6f 100644 --- a/tests/test_tool_rag_keyword_hints.py +++ b/tests/test_tool_rag_keyword_hints.py @@ -40,6 +40,14 @@ def test_tell_in_web_query_does_not_force_email_tools(): assert "web_search" in tools and "web_fetch" in tools +def test_explicit_web_search_query_gets_web_tools_without_retrieval(): + """Explicit web-search phrasing must surface web tools even if embeddings + return nothing.""" + ti = _index_without_embeddings() + tools = ti.get_tools_for_query("use web search and find a recipe for chocolate chip cookies") + assert "web_search" in tools and "web_fetch" in tools + + def test_genuine_email_query_still_gets_email_tools(): """Removing 'tell' must not break real email intent — the actual email keywords still force-include the toolset."""