mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: read script looks cheap first and only a decline buys the whole page
This commit is contained in:
@@ -19,6 +19,12 @@ logger = logging.getLogger(__name__)
|
||||
ToolRunner = Callable[[str, Dict, str, str], Awaitable[Dict]]
|
||||
|
||||
P_MIN_PAGE_CHARS = 500
|
||||
# First look: the same modest slice the main loop reads. Most pages answer from this.
|
||||
FIRST_READ_CHARS = 15000
|
||||
# Second look, only after a decline: everything we can actually use. Half our live reads used to
|
||||
# arrive pinned at exactly the smaller cap, with the answer (a reddit thread's comment scores sit
|
||||
# after the post body) sitting just past the cut, so the aux declined and a 100-220s model loop went
|
||||
# scrolling for text we had truncated ourselves.
|
||||
MAX_PAGE_CHARS = 24000
|
||||
P_TEXT_TIMEOUT_S = 8.0
|
||||
P_AUX_TIMEOUT_S = 12.0
|
||||
@@ -102,16 +108,13 @@ async def run_read_script(
|
||||
try:
|
||||
from backend.apps.agents.core.aux_llm import safe_resp_text
|
||||
|
||||
async def p_page_text() -> tuple:
|
||||
async def p_page_text(cap: int) -> tuple:
|
||||
"""Page text, but only once two consecutive reads agree it has stopped growing."""
|
||||
prev = -1
|
||||
text, url = "", ""
|
||||
for attempt in range(MAX_READS):
|
||||
# Ask for the whole budget we can actually use. The handler's default is sized for the
|
||||
# main loop's context, not for this one aux call, and taking that default meant half
|
||||
# our reads arrived pre-truncated with the answer sitting just past the cut.
|
||||
r = await asyncio.wait_for(
|
||||
execute_tool("BrowserGetText", {"max_chars": MAX_PAGE_CHARS}, browser_id, tab_id),
|
||||
execute_tool("BrowserGetText", {"max_chars": cap}, browser_id, tab_id),
|
||||
timeout=P_TEXT_TIMEOUT_S)
|
||||
text = str(r.get("text") or "") if isinstance(r, dict) and "error" not in r else ""
|
||||
url = str(r.get("url") or "") if isinstance(r, dict) else ""
|
||||
@@ -125,7 +128,13 @@ async def run_read_script(
|
||||
return (text, url) if len(text) >= P_MIN_PAGE_CHARS else ("", "")
|
||||
|
||||
for ask in range(1 + P_INSUFFICIENT_RETRIES):
|
||||
page, p_live_url = await p_page_text()
|
||||
# Cheap read first, full read only if that one came up short. A search-results page is one
|
||||
# we ALWAYS leave (the answer is a click deeper), so buying the whole thing up front is
|
||||
# text we can never answer from. Note the wall-clock case for this is NOT proven: live
|
||||
# sweeps of these tasks vary 5-12x run to run, which buries an effect this size. It stands
|
||||
# on the shape alone, never pay for what you don't need, and the retry is where the big
|
||||
# read earns its keep, on a page that IS the right page but got cut off.
|
||||
page, p_live_url = await p_page_text(FIRST_READ_CHARS if ask == 0 else MAX_PAGE_CHARS)
|
||||
if len(page) < P_MIN_PAGE_CHARS:
|
||||
logger.info(f"[browser-readscript] page too thin ({len(page)} chars); loop runs")
|
||||
return None
|
||||
|
||||
@@ -165,25 +165,40 @@ def test_a_page_that_simply_lacks_the_field_is_still_an_answer():
|
||||
assert rs.is_answer(answer) == answer, answer
|
||||
|
||||
|
||||
def test_the_read_asks_for_its_whole_budget_not_the_loops_default():
|
||||
"""The handler's default char cap is sized for the MAIN model loop's context, not for this one
|
||||
cheap aux call. Taking that default silently truncated the page: measured, 9 of 18 live reads
|
||||
came back at EXACTLY the cap, and on a reddit thread the comment scores sit past the post body,
|
||||
so the answer was cut off and a 100-220s model loop went looking for what we had removed."""
|
||||
asked = {}
|
||||
def test_the_first_look_is_cheap_and_only_a_decline_buys_the_big_read():
|
||||
"""Reading the whole page every time made a search-results page (which we ALWAYS leave, one
|
||||
click deeper) pay for chars it could never answer from: amazon's median went 26.1s -> 58.5s.
|
||||
So the first look is the modest slice and only an INSUFFICIENT escalates."""
|
||||
caps = []
|
||||
|
||||
async def run_tool(name, params, browser_id, tab_id):
|
||||
asked.update(params or {})
|
||||
caps.append((params or {}).get("max_chars"))
|
||||
return {"text": PAGE, "url": "https://www.reddit.com/r/x/comments/1/y"}
|
||||
|
||||
aux = Aux("The top comment is by u/someone with 387 upvotes.")
|
||||
out = asyncio.run(rs.run_read_script(aux, "m", "top comment?", "b1", "t1", run_tool))
|
||||
assert out == "The top comment is by u/someone with 387 upvotes."
|
||||
assert asked.get("max_chars") == rs.MAX_PAGE_CHARS, (
|
||||
f"read must ask for the {rs.MAX_PAGE_CHARS} chars it can use, asked {asked!r}")
|
||||
assert set(caps) == {rs.FIRST_READ_CHARS}, f"an answered page must never buy the big read, got {caps}"
|
||||
|
||||
|
||||
def test_a_decline_escalates_to_the_full_page():
|
||||
"""The big read exists for the page that IS the right page but was cut off. On a decline we
|
||||
re-read at the full budget before giving the task up to the model loop."""
|
||||
caps = []
|
||||
|
||||
async def run_tool(name, params, browser_id, tab_id):
|
||||
caps.append((params or {}).get("max_chars"))
|
||||
return {"text": PAGE, "url": "https://www.reddit.com/r/x/comments/1/y"}
|
||||
|
||||
aux = Aux("INSUFFICIENT")
|
||||
out = asyncio.run(rs.run_read_script(aux, "m", "top comment?", "b1", "t1", run_tool))
|
||||
assert out is None # still fails open to the loop
|
||||
assert rs.FIRST_READ_CHARS in caps # cheap first
|
||||
assert rs.MAX_PAGE_CHARS in caps, f"a decline must escalate to the full page, got {caps}"
|
||||
|
||||
|
||||
def test_the_budget_it_asks_for_is_the_budget_it_sends():
|
||||
"""If the ask and the send drift apart we are either paying for text we discard, or discarding
|
||||
text we paid for. They are the same number by construction."""
|
||||
assert rs.MAX_PAGE_CHARS >= 24000
|
||||
assert rs.FIRST_READ_CHARS < rs.MAX_PAGE_CHARS
|
||||
|
||||
Reference in New Issue
Block a user