[eric] browser: auto-dismiss junk interstitials (cookie/upsell/coachmark) by generic close-vocabulary, conservative

This commit is contained in:
ciregenz
2026-06-06 12:05:37 -07:00
parent d4e4327f78
commit 581af5b128
3 changed files with 87 additions and 0 deletions
@@ -36,6 +36,7 @@ from backend.apps.agents.browser.browser_loop import (
card_is_unavailable,
completion_is_honest,
deliverable_is_informational,
interstitial_dismiss_target,
replay_recheck_is_safe,
stagnation_exhausted,
)
@@ -475,6 +476,7 @@ async def run_browser_agent(
# auto candidate scan: aux-read results pages so pick-a-candidate happens in
# the same turn as the landing, not a read-then-decide pair later
auto_scanned_urls: set[str] = set()
dismissed_popup_urls: set[str] = set() # interstitials auto-closed, once per URL
auto_scan_count = 0
llm_ms_total = 0
out_tokens_total = 0 # sum of per-turn output tokens (the latency driver)
@@ -1435,6 +1437,29 @@ async def run_browser_agent(
# later solo re-list is still caught as redundant.
fresh_state_pending = True
# Auto-dismiss a blocking junk popup (cookie wall / upsell /
# coachmark) before it costs the model a turn. Mechanical, once
# per URL, only on the tight throwaway-dismiss vocabulary that
# never sits on a task-needed control, so it can't close anything
# required. After closing, re-list so the model sees the page beneath.
if tu.name in _AUTO_STATE_TOOLS and "error" not in result:
_pop_url = (result.get("url") or last_seen_url or "").split("#")[0]
if _pop_url and _pop_url not in dismissed_popup_urls:
_close = interstitial_dismiss_target("\n".join(attached_state_seen))
if _close:
dismissed_popup_urls.add(_pop_url)
_dres = await _cancellable(execute_browser_tool(
"BrowserClickByName", {"name": _close}, browser_id, tab_id))
_dok = isinstance(_dres, dict) and "error" not in _dres
logger.info(f"[browser-popup {session_id}] auto-dismissed '{_close}' "
f"ok={_dok} on {_pop_url[:80]}")
if _dok:
_fresh = await _post_action_state(
"BrowserClickByName", {}, _dres or {}, browser_id, tab_id,
_wait_exec, current_next_goal, seen_lines=attached_state_seen)
result["text"] = (f"{result.get('text') or ''}\n\n[auto] Closed a blocking "
f"popup ('{_close}'); the page beneath is now active.{_fresh}")
# Auto candidate scan: landing on a results-shaped page normally
# costs a read-then-decide turn pair; the cheap aux model reads it
# now so the pick happens on this same turn. Capped, per-URL,
@@ -32,6 +32,41 @@ _LOOP_REPEAT_THRESHOLD = 2 # the SECOND identical (tool,input,result) is alread
_LOOP_HARD_CAP = 5
# Universal close-affordance vocabulary for blocking popups (cookie walls,
# upsells, app-install nags, coachmarks). These phrases sit on a throwaway
# dismiss and NEVER on a control a real task needs (you never "No thanks" your
# way through a send), so a mechanical dismiss of one cannot close something the
# task required. Deliberately omits generic "Close"/"Dismiss"/"Skip", which DO
# appear on needed dialogs (e.g. "Close your conversation"). Keys on the pattern,
# not any one site, so it generalizes.
_DISMISS_NAMES = frozenset({
"no thanks", "no, thanks", "maybe later", "not now", "skip for now",
"remind me later", "got it", "decline", "no, maybe later", "not interested",
})
# never dismiss anything that smells like security or a real decision
_DANGER_NAME_RE = re.compile(r"verif|confirm|2fa|password|sign|pay|delete|send|post|submit", re.I)
_ROW_RE = re.compile(r'<\s*([a-z]+)\s+"([^"]*)"', re.I) # matches a [i]<role "name"> row
def interstitial_dismiss_target(interactives_text: str) -> str | None:
"""The accessible name of an unambiguous junk-popup close control on the
page, or None. Conservative by construction: matches only throwaway-dismiss
vocabulary that never sits on a task-needed control, on a button/link, and
never anything with security/confirm/commit wording, so a mechanical dismiss
can never close a dialog the task actually required."""
for line in (interactives_text or "").splitlines():
m = _ROW_RE.search(line)
if not m:
continue
role, name = m.group(1).lower(), m.group(2).strip()
if role not in ("button", "link"):
continue
norm = re.sub(r"[^a-z, ]", "", name.lower()).strip()
if norm in _DISMISS_NAMES and not _DANGER_NAME_RE.search(name):
return name
return None
def _hash_tool_call(tool_name: str, tool_input: dict, result: dict) -> tuple[str, str, str]:
"""Build a stable hash key for a tool call, including its result.
+27
View File
@@ -1301,3 +1301,30 @@ def test_informational_gate_strips_outcome_boilerplate_on_tie_break():
assert not deliverable_is_informational(short_action, "")
listy = "Found these:\n- a\n- b\n- c"
assert deliverable_is_informational(listy, "")
def test_interstitial_dismiss_target_generalizable_and_safe():
from backend.apps.agents.browser.browser_loop import interstitial_dismiss_target
# a junk popup with a throwaway-dismiss control gets found (any site)
page = '\n'.join([
'[3]<button "Try Premium for free">',
'[4]<button "No thanks">',
'[9]<textbox "Write a message…">',
])
assert interstitial_dismiss_target(page) == "No thanks"
# cookie/upsell variants
assert interstitial_dismiss_target('[1]<button "Maybe later">') == "Maybe later"
assert interstitial_dismiss_target('[1]<button "Not now">') == "Not now"
assert interstitial_dismiss_target('[1]<link "Got it">') == "Got it"
# NEVER dismisses task-needed or security/commit controls
assert interstitial_dismiss_target('[1]<button "Send">') is None
assert interstitial_dismiss_target('[1]<button "Message">') is None
assert interstitial_dismiss_target('[1]<button "Close your conversation with Tyler">') is None
assert interstitial_dismiss_target('[1]<button "Confirm">') is None
assert interstitial_dismiss_target('[1]<button "Verify your identity">') is None
# generic "Close"/"Dismiss"/"Skip" are NOT matched (they sit on needed dialogs)
assert interstitial_dismiss_target('[1]<button "Close">') is None
assert interstitial_dismiss_target('[1]<button "Skip">') is None
# empty / no rows
assert interstitial_dismiss_target('') is None
assert interstitial_dismiss_target('just some text') is None