mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: the read script asks for the whole page, not the model loop's smaller default
This commit is contained in:
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
ToolRunner = Callable[[str, Dict, str, str], Awaitable[Dict]]
|
||||
|
||||
P_MIN_PAGE_CHARS = 500
|
||||
P_MAX_PAGE_CHARS = 24000
|
||||
MAX_PAGE_CHARS = 24000
|
||||
P_TEXT_TIMEOUT_S = 8.0
|
||||
P_AUX_TIMEOUT_S = 12.0
|
||||
# Prestage's click often lands here while the SPA is still hydrating (measured: a
|
||||
@@ -107,8 +107,12 @@ async def run_read_script(
|
||||
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", {}, browser_id, tab_id), timeout=P_TEXT_TIMEOUT_S)
|
||||
execute_tool("BrowserGetText", {"max_chars": MAX_PAGE_CHARS}, 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 ""
|
||||
if len(text) >= P_MIN_PAGE_CHARS and 0 <= prev <= len(text) <= prev * (1 + P_STABLE_GROWTH):
|
||||
@@ -131,7 +135,7 @@ async def run_read_script(
|
||||
aux_client.messages.create(
|
||||
model=aux_model, max_tokens=500, temperature=0, system=P_SYSTEM,
|
||||
messages=[{"role": "user", "content": (
|
||||
f"Request: {task[:1200]}\n\nPage text:\n{page[:P_MAX_PAGE_CHARS]}")}],
|
||||
f"Request: {task[:1200]}\n\nPage text:\n{page[:MAX_PAGE_CHARS]}")}],
|
||||
), timeout=P_AUX_TIMEOUT_S))
|
||||
ms = int((time.monotonic() - t0) * 1000)
|
||||
answer = is_answer(reply)
|
||||
|
||||
@@ -163,3 +163,27 @@ def test_a_page_that_simply_lacks_the_field_is_still_an_answer():
|
||||
"Title: 'How to open a jar'. Channel: Kitchen Tips. 1.2M views.",
|
||||
):
|
||||
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 = {}
|
||||
|
||||
async def run_tool(name, params, browser_id, tab_id):
|
||||
asked.update(params or {})
|
||||
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}")
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -257,9 +257,20 @@ async function evalViaExecuteJs(wv: BrowserWebview, code: string): Promise<any>
|
||||
return first.value;
|
||||
}
|
||||
|
||||
async function handleGetText(wv: BrowserWebview): Promise<Record<string, any>> {
|
||||
// What the MAIN model loop gets. Every read lands in its context, so this stays modest on purpose.
|
||||
const GET_TEXT_DEFAULT_CHARS = 15000;
|
||||
// Ceiling for a caller that asks for more. The read script (one cheap aux call with its own context)
|
||||
// asks for its full budget: measured, 9 of 18 of its reads came back at EXACTLY 15000 chars, meaning
|
||||
// cut off, and on a reddit thread the comment scores sit after the post body, so they were never in
|
||||
// the text it was handed. It then declined, and a 100-220s model loop went scrolling for what we had
|
||||
// truncated ourselves.
|
||||
const GET_TEXT_MAX_CHARS = 30000;
|
||||
|
||||
async function handleGetText(wv: BrowserWebview, params: Record<string, any> = {}): Promise<Record<string, any>> {
|
||||
const asked = Number(params.max_chars) || GET_TEXT_DEFAULT_CHARS;
|
||||
const cap = Math.min(Math.max(asked, 1000), GET_TEXT_MAX_CHARS);
|
||||
const text: string = await evalInPage(wv,
|
||||
'document.body.innerText.substring(0, 15000)'
|
||||
`document.body.innerText.substring(0, ${cap})`
|
||||
);
|
||||
// Sampled HERE (on a read), not on navigate: by the time the agent reads the page, the SPA's XHR/fetch have fired, so routes are actually captured.
|
||||
const routes_available = await countSafeRoutes(wv);
|
||||
@@ -2028,7 +2039,7 @@ async function runBrowserCommand(
|
||||
result = await handleScreenshot(wv, params);
|
||||
break;
|
||||
case 'get_text':
|
||||
result = await handleGetText(wv);
|
||||
result = await handleGetText(wv, params);
|
||||
break;
|
||||
case 'get_console':
|
||||
result = await handleGetConsole(wv);
|
||||
|
||||
Reference in New Issue
Block a user