mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 20:29:56 +02:00
[eric] send: when the task names a field, write into that field (reddit's title)
This commit is contained in:
@@ -292,9 +292,43 @@ def opener_contradicts_task(task: str, opener_name: str) -> bool:
|
||||
return responding and bool(P_CREATE_OPENER_RE.search(name))
|
||||
|
||||
|
||||
def composer_index_in_state(state_text: str):
|
||||
"""(index, name) of the single compose-shaped textbox, or None. Two
|
||||
candidates = ambiguous = model's problem."""
|
||||
# Fields a person names out loud that a compose-shaped picker will never choose, because they are
|
||||
# short single-line inputs and nothing about them looks like a composer. Deliberately excludes
|
||||
# "body"/"message"/"comment": those ARE compose-shaped, the existing picker already finds them, and
|
||||
# adding them here would only create a second way to pick the same box.
|
||||
P_FIELD_HINT_RE = re.compile(r"\b(title|subject|headline)\b", re.I)
|
||||
|
||||
|
||||
def hinted_field_in_state(state_text: str, task: str):
|
||||
"""(index, name) of the textbox the TASK named by field, or None.
|
||||
|
||||
Measured live on reddit 2026-08-05: the task was 'create a text post whose title is exactly X',
|
||||
the picker below chose 'Post body text field' (correctly, by its own compose-shape rule), the
|
||||
Title stayed empty, and reddit's submit therefore stayed DISABLED on every attempt. The task had
|
||||
already said which field it meant, and nobody read it.
|
||||
|
||||
Not a guess: the field word comes from the user's own sentence and has to appear in the
|
||||
element's accessible name. Ambiguity still stands down, same rule as everywhere else here.
|
||||
"""
|
||||
m = P_FIELD_HINT_RE.search(task or "")
|
||||
if not m:
|
||||
return None
|
||||
word = m.group(1).lower()
|
||||
hits = [(int(i), n) for i, n in P_COMPOSER_ROW_RE.findall(state_text or "")
|
||||
if word in (n or "").lower()]
|
||||
return hits[0] if len(hits) == 1 else None
|
||||
|
||||
|
||||
def composer_index_in_state(state_text: str, task: str = ""):
|
||||
"""(index, name) of the textbox to write into, or None. Two candidates = ambiguous =
|
||||
model's problem.
|
||||
|
||||
A field the task named by name wins over the compose-shaped guess, because it is the one piece
|
||||
of evidence here that came from the person asking rather than from the page's shape.
|
||||
"""
|
||||
hinted = hinted_field_in_state(state_text, task)
|
||||
if hinted:
|
||||
return hinted
|
||||
hits = [(int(m.group(1)), m.group(2)) for m in P_COMPOSER_ROW_RE.finditer(state_text or "")
|
||||
if P_COMPOSER_NAME_RE.search(m.group(2) or "")]
|
||||
return hits[0] if len(hits) == 1 else None
|
||||
|
||||
@@ -340,7 +340,7 @@ async def run_send_script(
|
||||
return None
|
||||
log: list[dict] = []
|
||||
|
||||
composer = browser_send_parse.composer_index_in_state(state_text)
|
||||
composer = browser_send_parse.composer_index_in_state(state_text, task_sans_brief)
|
||||
if composer and browser_send_parse.surface_mismatch(task_sans_brief, composer[1]):
|
||||
# Asked to POST and found a COMMENT box, or found a DM box nobody asked for: either way it
|
||||
# is someone else's surface, not a slower route to ours. Drop it and let the tiers below
|
||||
@@ -352,7 +352,7 @@ async def run_send_script(
|
||||
for wait_s in (0.0, 1.2, 1.4):
|
||||
await asyncio.sleep(wait_s)
|
||||
fresh = await fresh_list()
|
||||
composer = browser_send_parse.composer_index_in_state(fresh)
|
||||
composer = browser_send_parse.composer_index_in_state(fresh, task_sans_brief)
|
||||
if composer:
|
||||
state_text = fresh
|
||||
break
|
||||
@@ -393,7 +393,7 @@ async def run_send_script(
|
||||
for wait_s in (0.0, 1.2, 1.5, 2.0, 2.0, 2.0):
|
||||
await asyncio.sleep(wait_s)
|
||||
state_text = await fresh_list()
|
||||
composer = browser_send_parse.composer_index_in_state(state_text)
|
||||
composer = browser_send_parse.composer_index_in_state(state_text, task_sans_brief)
|
||||
if composer:
|
||||
break
|
||||
p_settled = p_settled + 1 if state_text and state_text == p_prev else 0
|
||||
@@ -495,7 +495,8 @@ async def run_send_script(
|
||||
if wait_s:
|
||||
await asyncio.sleep(wait_s)
|
||||
state_retry = await fresh_list()
|
||||
composer_retry = browser_send_parse.composer_index_in_state(state_retry)
|
||||
composer_retry = browser_send_parse.composer_index_in_state(
|
||||
state_retry, task_sans_brief)
|
||||
if composer_retry:
|
||||
break
|
||||
if not composer_retry:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""When the task names a field, write into THAT field.
|
||||
|
||||
Measured live on reddit 2026-08-05, twice in one round. Task: "create a text post whose title is
|
||||
exactly canary5e95c195. Submit it." The compose-shaped picker chose 'Post body text field', which is
|
||||
the right answer to the question it was asking (which box looks like a composer?) and the wrong
|
||||
answer to the question that mattered (which box did the person mean?). reddit's Title stayed empty,
|
||||
its submit stayed DISABLED, and the run could never complete: `submit 'post' is present but
|
||||
DISABLED; the form is incomplete, handing to the model`, on every attempt.
|
||||
|
||||
The field word comes from the user's own sentence and must appear in the element's accessible name,
|
||||
so this is not the picker guessing; it is the picker finally reading the instruction.
|
||||
"""
|
||||
|
||||
from backend.apps.agents.browser import browser_send_parse as bp
|
||||
|
||||
# A reddit submit form as the perception lists it: a short Title input and the big body composer.
|
||||
REDDIT = '[21]<textbox "Title" />\n[23]<textbox "Post body text field" />\n[25]<button "post" />'
|
||||
# LinkedIn's post modal: one compose-shaped box, no named field anywhere.
|
||||
LINKEDIN = '[54]<textbox "Text editor for creating content" />\n[57]<button "Post" />'
|
||||
|
||||
|
||||
def test_a_named_title_beats_the_compose_shaped_guess():
|
||||
"""The regression, exactly as it happened."""
|
||||
assert bp.composer_index_in_state(
|
||||
REDDIT, 'create a text post whose title is exactly "canary5e95c195"') == (21, "Title")
|
||||
|
||||
|
||||
def test_without_a_field_word_the_old_picker_still_decides():
|
||||
"""No hint means no change: every site that worked before must keep working."""
|
||||
assert bp.composer_index_in_state(
|
||||
REDDIT, 'create a text post with body "hello"') == (23, "Post body text field")
|
||||
assert bp.composer_index_in_state(
|
||||
LINKEDIN, 'create a post with exactly this text: "hello"') == (
|
||||
54, "Text editor for creating content")
|
||||
|
||||
|
||||
def test_a_field_word_with_no_matching_box_changes_nothing():
|
||||
"""LinkedIn has no Title field. Naming one must not break the composer it does have."""
|
||||
assert bp.composer_index_in_state(
|
||||
LINKEDIN, 'post with the title "hello"') == (54, "Text editor for creating content")
|
||||
|
||||
|
||||
def test_two_boxes_matching_the_hint_stand_down():
|
||||
"""Ambiguity is the model's problem here, same as everywhere else in this file."""
|
||||
two = '[1]<textbox "Title" />\n[2]<textbox "Subtitle title" />\n[3]<textbox "Post body text field" />'
|
||||
assert bp.hinted_field_in_state(two, 'post with the title "x"') is None
|
||||
# and the compose-shaped picker still answers
|
||||
assert bp.composer_index_in_state(two, 'post with the title "x"') == (3, "Post body text field")
|
||||
|
||||
|
||||
def test_the_hint_words_stay_off_compose_shaped_fields():
|
||||
"""'body', 'message' and 'comment' are deliberately NOT hints: those boxes are already found by
|
||||
shape, and a second route to the same element is a second thing to keep in sync."""
|
||||
for word in ("body", "message", "comment"):
|
||||
assert bp.P_FIELD_HINT_RE.search(word) is None
|
||||
|
||||
|
||||
def test_the_old_single_argument_call_still_works():
|
||||
"""Callers outside the send script pass state only."""
|
||||
assert bp.composer_index_in_state(LINKEDIN) == (54, "Text editor for creating content")
|
||||
Reference in New Issue
Block a user