mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 09:47:44 +02:00
[eric] browser: a clean success needs an explicit Done; recognize gather asks, steer them to BrowserExtract
This commit is contained in:
@@ -2038,9 +2038,11 @@ async def run_browser_agent(
|
||||
"browser_id": browser_id,
|
||||
"summary": summary,
|
||||
# structured success signal the parent reads (replaces grepping the
|
||||
# summary for a tag): true only if the run is honest AND, when the
|
||||
# model called Done, it reported success. No-Done runs lean on honest.
|
||||
"done": honest and (done_success if done_called else True),
|
||||
# summary for a tag): true ONLY when the model explicitly finished via
|
||||
# Done with success AND the honesty gate agreed. A run that just stopped
|
||||
# (max turns, gave up, never called Done) is NOT a clean success, so the
|
||||
# fast path recovers instead of shipping a silent half-finish.
|
||||
"done": honest and done_called and done_success,
|
||||
# surface the honest failure to the parent so it doesn't treat a
|
||||
# did-nothing run as a success it can build on
|
||||
**({} if honest else {"error": summary}),
|
||||
|
||||
@@ -308,7 +308,8 @@ def replay_recheck_is_safe(action_log: list[dict]) -> bool:
|
||||
# never replay (the answer must be fresh), an action ask can.
|
||||
_INFO_ASK_RE = re.compile(
|
||||
r"\b(tell me|what(?:'s| is| are)|how (?:many|much)|count|list|summari[sz]e|"
|
||||
r"extract|find out|read (?:me|the)|get the|give me|which|who (?:is|are)|report back)\b",
|
||||
r"extract|find (?:me|out)|show me|look up|read (?:me|the)|get the|give me|which|"
|
||||
r"who (?:is|are)|report back|most (?:viewed|popular|liked|recent|rated|watched)|top \d+)\b",
|
||||
re.I,
|
||||
)
|
||||
_ACTION_ASK_RE = re.compile(
|
||||
|
||||
@@ -816,7 +816,15 @@ SYSTEM_PROMPT = (
|
||||
"- To pull SPECIFIC FIELDS off a page (names + links from results, prices, table "
|
||||
"rows), call BrowserExtract with what you want; a helper model reads the page and "
|
||||
"hands you just the JSON. One call replaces BrowserGetText plus you reading 15k "
|
||||
"chars, so prefer it whenever you know the fields you're after.\n\n"
|
||||
"chars, so prefer it whenever you know the fields you're after.\n"
|
||||
"- For ANY 'find me / list / get all / top N / most-viewed' gathering task, "
|
||||
"BrowserExtract IS your tool: state the fields and let the helper read the rendered "
|
||||
"page. Do NOT hand-roll CSS/DOM selectors in BrowserEvaluate to scrape a list, modern "
|
||||
"sites (YouTube, LinkedIn, Amazon) obfuscate and lazy-render class names, so selector "
|
||||
"scraping burns turns and silently returns the wrong nodes. If one BrowserEvaluate read "
|
||||
"comes back empty or shaped wrong, STOP, that is your signal to switch to BrowserExtract, "
|
||||
"not to debug another selector. The answer must end up IN your Done message, so gather it "
|
||||
"for real; never report done on a gather task you couldn't actually read.\n\n"
|
||||
|
||||
"## When you genuinely cannot proceed\n"
|
||||
"Use RequestHumanIntervention for:\n"
|
||||
|
||||
@@ -267,6 +267,21 @@ def test_done_tool_success_false_marks_not_done(monkeypatch):
|
||||
assert "login wall" in result["summary"]
|
||||
|
||||
|
||||
def test_run_that_never_calls_done_is_not_a_clean_success(monkeypatch):
|
||||
# A run that does real work but stops with plain text (never calls Done) is a
|
||||
# half-finish, not a clean success: done must be False so the fast path recovers
|
||||
# instead of shipping a silent stop (the 'Task completed.' that wasn't).
|
||||
BH._browser_history.clear(); BH._domain_notes.clear()
|
||||
primary = FakeLLM([
|
||||
Resp([_rp("click it"), _tu("BrowserClickIndex", index=3)]),
|
||||
Resp([Blk("text", "I clicked the thing.")], stop_reason="end_turn"),
|
||||
])
|
||||
aux = FakeAux()
|
||||
_install(monkeypatch, primary, aux)
|
||||
result = asyncio.run(BA.run_browser_agent(task="open the settings page", browser_id="b1", model="sonnet"))
|
||||
assert result.get("done") is False # no explicit Done -> not a clean success
|
||||
|
||||
|
||||
def test_early_perception_is_not_cut_short_before_any_action(monkeypatch):
|
||||
# Orienting on a cold/slow page can take several look-only turns; the stall
|
||||
# backstop must NOT fire before the agent has done anything (it only bounds a
|
||||
@@ -1402,6 +1417,16 @@ def test_informational_gate_strips_outcome_boilerplate_on_tie_break():
|
||||
assert deliverable_is_informational(listy, "")
|
||||
|
||||
|
||||
def test_find_me_and_most_viewed_asks_are_informational():
|
||||
from backend.apps.agents.browser.browser_loop import deliverable_is_informational
|
||||
# the exact MKBHD task that previously slipped past the gate ("find me" + "most viewed")
|
||||
assert deliverable_is_informational("", "find me his 50 most viewed vids")
|
||||
assert deliverable_is_informational("", "show me the top 10 trending repos")
|
||||
assert deliverable_is_informational("", "look up the cheapest flight")
|
||||
# a pure action ask still is not informational
|
||||
assert not deliverable_is_informational("", "send Tyler a message saying hi")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user