[eric] browser: open the first post by its own link, instead of hunting a feed

This commit is contained in:
ciregenz
2026-08-04 23:26:37 -07:00
parent ac6dbb119f
commit 324639e217
3 changed files with 159 additions and 0 deletions
@@ -19,6 +19,7 @@ import time
from typing import Awaitable, Callable
from backend.apps.agents.browser import browser_send_parse, compose_discovery, compose_entry
from backend.apps.agents.browser import first_item_of_type
from backend.apps.agents.browser.strip_lone_surrogates import strip_lone_surrogates
logger = logging.getLogger(__name__)
@@ -400,6 +401,7 @@ async def run_prestage(
p_results_overruled = False
p_composer_overruled = False
p_ctype_overruled = False
p_first_item_tried = False
while (not staged_complete and steps < p_max_steps
and (time.monotonic() - t0) < p_total_timeout):
# Per-step cost, broken out. Prestage is the largest single phase of a LinkedIn write
@@ -421,6 +423,36 @@ async def run_prestage(
f"{int((time.monotonic() - p_t_step) * 1000)}ms: a composer is already "
f"on the page, no aux call needed")
break
# TIER 1: the task asked for the FIRST <thing>, and the page's own links say which ones
# are that thing. Instagram posts are /p/, tiktok videos /video/, youtube /watch. Picking
# the first matching link is deterministic; asking a model to find it in a feed is not,
# and that is where instagram kept going wrong (1/10, 3/5, 0/5, 0/5 across four windows,
# landing in the stories viewer, on a profile, or nowhere). Costs one evaluate against
# the 1.7-6.5s aux call it replaces, and it can only ever choose a URL the downstream
# wrong-surface guard would also accept, because both read the same type patterns.
if task_is_send and not p_first_item_tried:
p_first_item_tried = True
p_want = first_item_of_type.wanted_type(task)
p_expr = first_item_of_type.first_link_expression(p_want) if p_want else ""
if p_expr and not browser_send_parse.content_type_mismatch(task, current_url):
try:
p_r = await asyncio.wait_for(
execute_tool("BrowserEvaluate", {"expression": p_expr},
browser_id, tab_id), timeout=6.0)
p_href = str((p_r or {}).get("value") or (p_r or {}).get("text") or "").strip()
except Exception:
p_href = ""
if p_href.startswith("http") and p_href != current_url:
logger.info(f"[browser-prestage] tier-1: first {p_want} is {p_href[:70]}")
r = await execute_tool("BrowserNavigate", {"url": p_href}, browser_id, tab_id)
if isinstance(r, dict) and "error" not in r:
recs.append({"tool": "BrowserNavigate", "input": {"url": p_href},
"ok": True, "result_summary": f"first {p_want}"[:200],
"elapsed_ms": 0})
done_desc.append(f"opened the first {p_want}")
current_url = p_href
steps += 1
continue
p_t_aux = time.monotonic()
reply = safe_resp_text(await asyncio.wait_for(
client.messages.create(
@@ -0,0 +1,66 @@
"""Open the first item of the kind the task named, without asking a model to find it.
"Comment on the first post" is a RESPOND task, so the compose-URL table (which answers "where do I
CREATE one of these?") does not apply, and the aux navigator has to find the item by clicking around
a feed. That is where instagram kept going wrong: measured across four windows it scored 1/10, 3/5,
0/5 and 0/5, landing variously in the stories viewer, on a profile, or nowhere, because the feed's
first clickable thing is not reliably a post.
But a site already publishes what each link IS, in the link's own URL. Instagram posts live at /p/,
tiktok videos at /video/, youtube at /watch, reddit threads at /comments/. So when the task names a
content type, we can pick the first link of that type deterministically instead of hoping the model
picks it. One page evaluate plus one navigate, no aux turn.
Generalises by construction: the patterns are the same P_URL_CONTENT_TYPES the wrong-surface guard
already uses, so a URL that would be REFUSED downstream as the wrong kind can never be chosen here.
"""
import re
from typing import Optional
from typeguard import typechecked
from backend.apps.agents.browser import browser_send_parse
# "the first post", "the top video", "the first story". Ordinal words only: without one the task is
# about a specific named thing, and picking the first anything would be a guess.
P_FIRST_RE = re.compile(r"\b(first|top|latest|newest|most recent)\b", re.I)
@typechecked
def wanted_type(task: str) -> Optional[str]:
"""The content type this task wants the first of, or None.
Requires BOTH an ordinal ("first", "top", "latest") and exactly one content-type word, so
"comment on the first post" qualifies and "reply to Sarah's message" does not: the second names
a specific target that a positional pick would get wrong.
"""
if not task or not P_FIRST_RE.search(task):
return None
hits = [name for name, rx in browser_send_parse.P_TASK_CONTENT_TYPES if rx.search(task)]
return hits[0] if len(hits) == 1 else None
@typechecked
def first_link_expression(content_type: str) -> str:
"""JS returning the first same-origin link whose URL says it is `content_type`, or "".
Same-origin on purpose: a feed is full of outbound links, and following one off-site turns a
"comment on the first post" into a visit to whatever an ad pointed at.
"""
pattern = {
"post": r"/(p|posts?|status)/",
"video": r"/(watch|shorts)\b|/video/",
"story": r"/stories?/",
"message": r"/(direct|messages?)/",
}.get(content_type, "")
if not pattern:
return ""
return (
"(() => { const rx = new RegExp(" + repr(pattern).replace("'", '"') + "); "
"for (const a of document.querySelectorAll('a[href]')) { "
" let u; try { u = new URL(a.href, location.href); } catch (e) { continue; } "
" if (u.origin !== location.origin) continue; "
" if (rx.test(u.pathname)) return u.href; } "
"return ''; })()"
)
+61
View File
@@ -0,0 +1,61 @@
"""Open the first item of the kind the task named, without asking a model to find it.
"Comment on the first post" is a RESPOND task, so the compose-URL table (which answers "where do I
CREATE one of these?") does not apply and the aux navigator has to find the item by clicking around a
feed. Measured on instagram across four windows: 1/10, 3/5, 0/5, 0/5, landing variously in the
stories viewer, on a profile, or nowhere, because a feed's first clickable thing is not reliably a
post.
But a site publishes what each link IS, in the link's own URL: instagram posts at /p/, tiktok videos
at /video/, youtube at /watch. Picking the first match is deterministic where a model is not.
The rules exist to stop it firing when a positional pick would be WRONG, which is the only way this
tier can hurt: it navigates, and navigating away from the right page costs the run.
"""
from backend.apps.agents.browser import first_item_of_type as fit
def test_an_ordinal_plus_one_content_type_qualifies():
assert fit.wanted_type("write a comment on the first post") == "post"
assert fit.wanted_type("comment on the first video") == "video"
assert fit.wanted_type("reply to the top story") == "story"
def test_a_named_target_is_never_a_positional_pick():
""""reply to Sarah's message" names WHICH one. Opening "the first message" would answer the
wrong person, which is the same family as the DM incident this codebase already has scars from."""
assert fit.wanted_type("reply to Sarah's message") is None
assert fit.wanted_type("comment on the post about rust") is None
def test_no_content_type_named_means_no_pick():
assert fit.wanted_type("post this tweet") is None
assert fit.wanted_type("send hi to charles") is None
def test_two_content_types_stay_ambiguous():
"""A task naming two kinds gives no basis for choosing which to open first."""
assert fit.wanted_type("reply to the first story on that post") is None
assert fit.wanted_type("comment on the first video or post") is None
def test_the_link_search_is_same_origin_only():
"""A feed is full of outbound links and ads. Following one turns "comment on the first post"
into a visit to whatever an advertiser paid for."""
expr = fit.first_link_expression("post")
assert "u.origin !== location.origin" in expr, "must refuse cross-origin links"
assert "querySelectorAll('a[href]')" in expr
def test_each_type_gets_the_shape_that_site_family_actually_uses():
assert "/(p|posts?|status)/" in fit.first_link_expression("post") # instagram, mastodon
assert "/(watch|shorts)" in fit.first_link_expression("video") # youtube
assert "/video/" in fit.first_link_expression("video") # tiktok
assert "/stories?/" in fit.first_link_expression("story")
def test_an_unknown_type_yields_no_expression_rather_than_a_broken_one():
"""No expression means the tier stands down and the aux navigator runs, i.e. today's behaviour."""
assert fit.first_link_expression("banana") == ""
assert fit.first_link_expression("") == ""