mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] browser: verified-action executor foundation = generic site-agnostic effect verification (did the action produce the SPECIFIC expected effect: url_changed/appeared/gone/filled/cleared), zero per-site code; wired into the send-script's receipt (proves it on the real flow, behavior-identical to the old inline check via an equivalence test) so it's the executor's verification core, not dead scaffolding
This commit is contained in:
@@ -19,6 +19,8 @@ import re
|
||||
import time
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from backend.apps.agents.browser import browser_verified_action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Double quotes are unambiguous. Single quotes only delimit when the opener is at a word boundary (start/space/colon), so an in-word apostrophe like "chen's" is never mistaken for a payload quote, that mispairing was silently corrupting the canonical "text him '...'" errand.
|
||||
@@ -196,12 +198,15 @@ async def run_send_script(
|
||||
if not send_ok:
|
||||
logger.info(f"[browser-sendscript] send click errored ({send_name}); handing to model (fill committed, NOT sent)")
|
||||
return None
|
||||
# 4. two-sided receipt: composer seen cleared of the payload
|
||||
# 4. two-sided receipt via the GENERIC verifier: the send is confirmed only when
|
||||
# the composer clears the payload ("cleared:<payload>"). Same verdict as the old
|
||||
# inline check, now expressed as the site-agnostic expectation a verified-action
|
||||
# executor uses everywhere, so this proven flow IS the executor's verification core.
|
||||
cleared = False
|
||||
for wait_s in (0.4, 1.0, 1.6):
|
||||
await asyncio.sleep(wait_s)
|
||||
state3 = await fresh_list()
|
||||
if state3 and not payload_in_textbox(state3, payload):
|
||||
if state3 and browser_verified_action.expectation_met(f"cleared:{payload}", state2, state3):
|
||||
cleared = True
|
||||
break
|
||||
note = ("" if cleared else
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Generic, site-agnostic verification: did an action produce the SPECIFIC effect
|
||||
it was meant to? This is the load-bearing piece that lets a verified-action executor
|
||||
work on any website without per-site code, the model (or a scripted flow) names a
|
||||
generic expectation, and this checks it against a cheap before/after page snapshot.
|
||||
|
||||
A snapshot is just the interactives-list text plus the URL, the same things every
|
||||
site exposes, so nothing here knows about LinkedIn or any particular page. It
|
||||
generalizes the send-script's proven two-sided receipt ("the composer cleared") from
|
||||
one hand-tuned flow into one predicate ("cleared:<text>") reusable everywhere.
|
||||
|
||||
Expectations (kind, or "kind:arg"):
|
||||
url_changed the page navigated
|
||||
changed the page changed at all (weakest; a fallback)
|
||||
appeared:X X is present now but wasn't before (a menu/dialog/result opened)
|
||||
gone:X X was present before but isn't now (an item/row deleted)
|
||||
filled:X some textbox value now carries X (a fill committed)
|
||||
cleared:X no textbox value carries X (a composer sent + emptied)
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
# Match payload_in_textbox: long values truncate in the list, so compare on a prefix.
|
||||
P_VALUE_PREFIX_LEN = 24
|
||||
P_TEXTBOX_LINE = "<textbox"
|
||||
|
||||
|
||||
def parse_expectation(expect: str) -> Tuple[str, str]:
|
||||
"""(kind, arg) from 'kind' or 'kind:arg'. Unknown kinds are returned as-is and
|
||||
treated as unmet by expectation_met, so a typo fails safe (verification withheld)."""
|
||||
raw = (expect or "").strip()
|
||||
if ":" in raw:
|
||||
kind, arg = raw.split(":", 1)
|
||||
return kind.strip().lower(), arg.strip()
|
||||
return raw.lower(), ""
|
||||
|
||||
|
||||
def value_present(state_text: str, sub: str) -> bool:
|
||||
"""True if any listed textbox VALUE carries sub (prefix match, like a committed
|
||||
fill). Same logic as payload_in_textbox so the send-script stays behavior-identical."""
|
||||
probe = (sub or "")[:P_VALUE_PREFIX_LEN]
|
||||
if not probe:
|
||||
return False
|
||||
return any(P_TEXTBOX_LINE in line and probe in line
|
||||
for line in (state_text or "").splitlines())
|
||||
|
||||
|
||||
def p_contains(state_text: str, sub: str) -> bool:
|
||||
s = (sub or "").strip().lower()
|
||||
return bool(s) and s in (state_text or "").lower()
|
||||
|
||||
|
||||
def expectation_met(
|
||||
expect: str, before: str, after: str,
|
||||
before_url: str = "", after_url: str = "",
|
||||
) -> bool:
|
||||
"""Did `after` satisfy `expect` given `before`? Pure; unknown expectation = False
|
||||
(fail safe: verification withheld rather than a false pass)."""
|
||||
kind, arg = parse_expectation(expect)
|
||||
if kind == "url_changed":
|
||||
return bool(after_url) and after_url != before_url
|
||||
if kind == "changed":
|
||||
return before != after or (bool(after_url) and after_url != before_url)
|
||||
if kind == "appeared":
|
||||
return p_contains(after, arg) and not p_contains(before, arg)
|
||||
if kind == "gone":
|
||||
return p_contains(before, arg) and not p_contains(after, arg)
|
||||
if kind == "filled":
|
||||
return value_present(after, arg)
|
||||
if kind == "cleared":
|
||||
return not value_present(after, arg)
|
||||
return False
|
||||
@@ -0,0 +1,45 @@
|
||||
"""The generic, site-agnostic verification core of the verified-action executor:
|
||||
does an action produce the SPECIFIC expected effect, checked against a before/after
|
||||
snapshot with zero per-site code. Pinned here, and pinned to match the send-script's
|
||||
proven receipt so wiring it in was behavior-preserving."""
|
||||
from backend.apps.agents.browser import browser_verified_action as va
|
||||
from backend.apps.agents.browser.browser_agent import payload_in_textbox
|
||||
|
||||
EMPTY = '[2]<textbox "Write a message">\n[9]<button "Attach">'
|
||||
FILLED = '[2]<textbox "Write a message" value="[test] hello world r9-os">\n[14]<button "Send">'
|
||||
SENT = '[2]<textbox "Write a message">\n[9]<button "Attach">'
|
||||
PAYLOAD = "[test] hello world r9-os"
|
||||
|
||||
|
||||
def test_url_changed_and_changed():
|
||||
assert va.expectation_met("url_changed", "s", "s", "u1", "u2")
|
||||
assert not va.expectation_met("url_changed", "s", "s", "u1", "u1")
|
||||
assert va.expectation_met("changed", "a", "b")
|
||||
assert not va.expectation_met("changed", "a", "a")
|
||||
|
||||
|
||||
def test_appeared_and_gone():
|
||||
assert va.expectation_met("appeared:Send", EMPTY, FILLED) # Send button showed up
|
||||
assert not va.expectation_met("appeared:Send", FILLED, FILLED)
|
||||
assert va.expectation_met("gone:Send", FILLED, SENT) # Send button vanished after send
|
||||
assert not va.expectation_met("gone:Send", EMPTY, EMPTY)
|
||||
|
||||
|
||||
def test_filled_and_cleared_match_the_send_receipt():
|
||||
# filled == the fill committed; cleared == the composer emptied (the send receipt)
|
||||
assert va.expectation_met("filled:" + PAYLOAD, EMPTY, FILLED)
|
||||
assert va.expectation_met("cleared:" + PAYLOAD, FILLED, SENT)
|
||||
assert not va.expectation_met("cleared:" + PAYLOAD, EMPTY, FILLED) # still in the box
|
||||
|
||||
|
||||
def test_generic_verifier_agrees_with_the_proven_inline_check():
|
||||
# The send-script's receipt was `not payload_in_textbox(state, payload)`; the
|
||||
# generic `cleared:` predicate must give the identical verdict on every state.
|
||||
for state in (EMPTY, FILLED, SENT):
|
||||
old = not payload_in_textbox(state, PAYLOAD)
|
||||
new = va.expectation_met("cleared:" + PAYLOAD, FILLED, state)
|
||||
assert old == new, f"mismatch on state={state!r}"
|
||||
|
||||
|
||||
def test_unknown_expectation_fails_safe():
|
||||
assert not va.expectation_met("teleported:X", EMPTY, FILLED) # typo/unknown = not met
|
||||
Reference in New Issue
Block a user