diff --git a/scripts/verify_clean_v3_search_quality.mjs b/scripts/verify_clean_v3_search_quality.mjs index c926354d0..d198575c4 100644 --- a/scripts/verify_clean_v3_search_quality.mjs +++ b/scripts/verify_clean_v3_search_quality.mjs @@ -10,6 +10,7 @@ const base = process.env.BASE_URL || 'http://127.0.0.1:7011'; const owner = 'sft_alex_creator'; const endpointId = process.env.ENDPOINT_ID || '1d1022ef'; const endpointUrl = process.env.ENDPOINT_URL || (() => { throw new Error("ENDPOINT_URL is required"); })(); +const model = process.env.MODEL || 'odysseus-qwen3.5-tools-pre-heretic'; const run = new Date().toISOString().replace(/[:.]/g, '-'); const reportPath = path.resolve(process.env.REPORT_PATH || path.join(root, `reports/clean-v3-search-quality-${run}.json`)); if (!reportPath.startsWith(path.join(root, 'reports') + path.sep) || fs.existsSync(reportPath)) throw Error('Report path must be new and under reports/'); @@ -31,7 +32,7 @@ const parseSSE = body => body.replace(/\r\n/g, '\n').split('\n\n').flatMap(frame async function createSession(context, name) { const response = await context.request.post(`${base}/api/session`, { multipart: { - name, model: 'odysseus-qwen3.5-tools-pre-heretic', endpoint_id: endpointId, + name, model, endpoint_id: endpointId, endpoint_url: endpointUrl, skip_validation: 'true', rag: 'false', }}); if (!response.ok()) throw Error(`Session create HTTP ${response.status()}`); diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index 14db85bee..598771651 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -4945,24 +4945,19 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac canonical(schema['function']['name']) == 'private_browser' for schema in offered ) - and re.search( - r'\b(?:HTTP\s+(?:401|403|429)|access\s+(?:denied|blocked)|' - r'captcha|needs?\s+JS|login|no\s+readable\s+text|timed?\s*out)\b', - output, - re.I, - ) + and retrieved_source_urls(args) ): # Static fetchers are routinely rejected by publisher # bot protection. That is a transport failure, not # evidence that the source is unavailable. Offer one # rendered-browser attempt at the same evidenced URL. suppressed_tool_until_round['web_fetch'] = round_number + 1 - fetch_url = str(args.get('url') or '').strip().rstrip('/') - if fetch_url: - static_fetch_failed_urls.add(fetch_url) + for fetch_url in retrieved_source_urls(args): + static_fetch_failed_urls.add(fetch_url.rstrip('/')) + force_private_browser_next_round = True round_recovery_messages.append( - 'The static page fetch was blocked or returned no readable content. ' - 'Use private_browser once to open the same known URL and inspect the ' + 'The static page fetch failed or returned no readable content. Use ' + 'private_browser once to open the strongest known URL and inspect the ' 'rendered page; if that also fails, report the limitation without ' 'inventing page content.' ) diff --git a/src/turn_contract.py b/src/turn_contract.py index 9cbc6b367..5bf5ad57c 100644 --- a/src/turn_contract.py +++ b/src/turn_contract.py @@ -671,6 +671,24 @@ def selected_tools_for_request(message: str) -> frozenset[str] | None: } if explicitly_named_web: return frozenset(explicitly_named_web) + if ( + re.search(r"\b(?:look\s*up|search|find)\b", text, re.I) + and re.search( + r"\b(?:current|latest|today(?:'s)?|right\s+now|this\s+(?:week|month|year))\b", + text, + re.I, + ) + and not re.search(r"\bhttps?://", text, re.I) + and not re.match( + r"^\s*" + _REQUEST_PREFIX + r"(?:open|browse|visit|navigate|go\s+to)\b", + text, + re.I, + ) + ): + # Current lookups need discovery before navigation. Letting the model + # begin on an arbitrary browser page can ground an answer in stale or + # unrelated content without ever establishing a current source set. + return frozenset({"web_search"}) if re.search( r"\buse\s+(?:the\s+)?(?:odysseus\s+)?web_search\b", raw_text, diff --git a/tests/test_turn_contract.py b/tests/test_turn_contract.py index 3aafdec9e..e3dfaa437 100644 --- a/tests/test_turn_contract.py +++ b/tests/test_turn_contract.py @@ -67,6 +67,15 @@ def test_latest_topic_info_and_common_news_typo_route_to_web_search(): assert requested_capabilities(prompt) == frozenset({"search_browser"}) +@pytest.mark.parametrize('prompt', [ + 'Look up the current stock market and summarize the major US indexes.', + 'Find the latest Nvidia driver for Linux.', + 'Search for today\'s exchange rate for USD to JPY.', +]) +def test_explicit_current_lookup_starts_with_native_search(prompt): + assert selected_tools_for_request(prompt) == frozenset({'web_search'}) + + @pytest.mark.parametrize("prompt", [ "I'm using a WIKING fireplace; can I find an English manual online?", "Can you look online",