mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] browser: auto-dismiss junk interstitials (cookie/upsell/coachmark) by generic close-vocabulary, conservative
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user