mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 11:42:23 +02:00
240 lines
12 KiB
Python
240 lines
12 KiB
Python
"""The site's own URL for opening a composer, used when the task is to create something new.
|
|
|
|
Reachability, not fill mechanics, is what a 20-run dry sweep measured as the ceiling on scripted
|
|
writes: the send script armed 20/20 and reached a composer 0/20. The fill, the commit check and the
|
|
two-sided receipt are all proven; what fails is that prestage's aux navigator lands on the site's
|
|
HOME page, and the composer is one more hop that it does not reliably take. Polling harder does not
|
|
help, it was tried twice and the reached rate went 40% -> 0%.
|
|
|
|
Every site here publishes a URL that opens its own composer. Asking for that URL is deterministic
|
|
where hunting for a button is not: no selector to drift, no modal to race, no capped element list to
|
|
starve. It is also cheaper, since a hit skips the aux navigation loop entirely.
|
|
|
|
Deliberately narrow, because composing in the wrong place is worse than not composing at all:
|
|
|
|
- only for a task that creates something TOP-LEVEL. A reply or comment belongs on the thread the
|
|
user is looking at, so those keep their own target (the same rule the post-is-not-a-comment
|
|
guard enforces on the composer, applied one layer earlier and one layer cheaper).
|
|
- only when the task points at the bare site. Any deeper URL on that host is a specific target
|
|
the user chose, and it outranks the generic composer every time.
|
|
- only as a PROPOSAL. The caller navigates, then checks whether a composer actually appeared; if
|
|
it did not, the normal path runs untouched. A site that changes its compose URL degrades to
|
|
today's behaviour rather than stranding the run somewhere useless.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from typing import Dict, List, Optional
|
|
from urllib.parse import urlparse
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.agents.browser import browser_fast_path, browser_send_parse
|
|
|
|
# Registrable host -> the site's own compose URL. A dynamic-key map keyed by host, matched by
|
|
# suffix so www./m./mobile. variants all resolve. Adding a site is one row.
|
|
#
|
|
# These are the sites' own documented entry points, not scraped links:
|
|
# x /compose/post opens the post dialog on a fresh page
|
|
# linkedin ?shareActive=true opens the "start a post" modal on the feed
|
|
# reddit /submit?type=TEXT the self-post form
|
|
# gmail ?compose=new opens a compose window in the mail UI
|
|
P_COMPOSE_URLS: Dict[str, str] = {
|
|
"x.com": "https://x.com/compose/post",
|
|
"twitter.com": "https://x.com/compose/post",
|
|
"linkedin.com": "https://www.linkedin.com/feed/?shareActive=true",
|
|
"reddit.com": "https://www.reddit.com/submit?type=TEXT",
|
|
"mail.google.com": "https://mail.google.com/mail/u/0/#inbox?compose=new",
|
|
}
|
|
|
|
# Creating something new. "compose"/"draft"/"email" carry gmail, where nobody says "post".
|
|
P_CREATE_RE = re.compile(
|
|
r"\b(post|posting|tweet|tweeting|publish|publishing|share|sharing|compose|composing|"
|
|
r"draft|drafting|write|writing|send|create|creating|start|starting|ask|asking)\b"
|
|
# "open a new issue/thread" is a create; bare "open" is navigation ("open the first video"),
|
|
# so only the whole phrase counts. Measured: github never reached the tier without this.
|
|
r"|\bopen(ing)? a new\b", re.I)
|
|
# Answering something that already exists. One of these and the target is the thread, not the site.
|
|
P_RESPOND_RE = re.compile(r"\b(reply|replies|replying|comment|commenting|respond|responding|"
|
|
r"answer|answering|quote|retweet|dm|message)\b", re.I)
|
|
P_URL_RE = re.compile(r"https?://[^\s\"'<>)\]]+", re.I)
|
|
# "go to x.com and post ..." names its site without a scheme, which is how most tasks arrive.
|
|
P_BARE_HOST_RE = re.compile(r"(?:^|[\s/@(,])((?:[\w-]+\.)+[a-z]{2,})\b", re.I)
|
|
|
|
|
|
@typechecked
|
|
def registrable_host(url_or_host: str) -> str:
|
|
"""The host with any www./m./mobile. prefix removed, lowercased, port dropped.
|
|
|
|
Prefix stripping is done with a real prefix check; `lstrip("www.")` would eat any leading w
|
|
or dot and quietly turn `w3schools.com` into `3schools.com`."""
|
|
raw = (url_or_host or "").strip()
|
|
host = urlparse(raw).netloc if "//" in raw else raw
|
|
host = host.lower().split("@")[-1].split(":")[0]
|
|
for prefix in ("www.", "m.", "mobile."):
|
|
if host.startswith(prefix):
|
|
host = host[len(prefix):]
|
|
break
|
|
return host
|
|
|
|
|
|
@typechecked
|
|
def p_table_hit(host: str) -> str:
|
|
"""The compose URL for this host, matching a parent domain too (`old.reddit.com` -> reddit).
|
|
|
|
Exact-or-dotted-suffix only: a bare `endswith` would match `notreddit.com` against `reddit.com`
|
|
and send a post to a site the user never named."""
|
|
host = registrable_host(host)
|
|
if not host:
|
|
return ""
|
|
if host in P_COMPOSE_URLS:
|
|
return P_COMPOSE_URLS[host]
|
|
for known, url in P_COMPOSE_URLS.items():
|
|
if host.endswith("." + known):
|
|
return url
|
|
return ""
|
|
|
|
|
|
@typechecked
|
|
def p_names_deeper_target(task: str, host: str) -> bool:
|
|
"""True when the task carries a URL on this host that points somewhere more specific than its
|
|
front page. That URL is the user's chosen target and must win over the generic composer."""
|
|
want = registrable_host(host)
|
|
for m in P_URL_RE.finditer(human_words(task)):
|
|
parsed = urlparse(m.group(0).rstrip(".,;)"))
|
|
found = registrable_host(parsed.netloc)
|
|
if not found or (found != want and not found.endswith("." + want)):
|
|
continue
|
|
if parsed.path.strip("/") or parsed.query or parsed.fragment:
|
|
return True
|
|
return False
|
|
|
|
|
|
@typechecked
|
|
def human_words(task: str) -> str:
|
|
"""The task minus the aux-written routing brief.
|
|
|
|
A dispatched task is the user's prompt followed by a brief a model wrote about how to route it.
|
|
The brief is prose, and it both quotes things of its own and uses answering words, so reading
|
|
intent off the whole string reads the model's commentary as the user's request. The send script
|
|
hit this first (a brief saying "do not submit it" read-only-flagged a real send).
|
|
|
|
Everything reads this, refusals included. Letting the brief veto looked like the safe choice
|
|
and was not: briefs routinely spell out a route ("navigate to https://x.com/home"), so the
|
|
deeper-target veto fired on the model's own suggestion and silently disabled the tier on two of
|
|
four sites. A brief cannot turn a post into a reply either, since the words that would say so
|
|
are the user's and are still read here."""
|
|
return (task or "").split(browser_fast_path.BRIEF_MARKER, 1)[0]
|
|
|
|
|
|
@typechecked
|
|
def wants_top_level_compose(task: str) -> bool:
|
|
"""True when the task creates something new rather than answering something that exists.
|
|
|
|
An intent word alone is not enough: "what is the top post on reddit" is a READ, and `post` there
|
|
is a noun. Reading the verb correctly needs a model, so instead this asks for the thing every
|
|
real write in this product carries and no read does, the quoted text to write. That is also the
|
|
send script's own precondition, so a task this refuses is one the script would decline anyway,
|
|
and the aux navigator handles it exactly as it does today."""
|
|
text = human_words(task)
|
|
if not browser_send_parse.quoted_payload(text):
|
|
return False
|
|
return bool(P_CREATE_RE.search(text)) and not P_RESPOND_RE.search(text)
|
|
|
|
|
|
@typechecked
|
|
def enabled() -> bool:
|
|
"""On by default. This tier only ever navigates the user's browser to a page that site
|
|
publishes for exactly this purpose, and the caller verifies the result before relying on it, so
|
|
the failure mode is a wasted page load rather than a wrong action. The switch exists to A/B it
|
|
against the aux navigator and to turn it off in the field without a rebuild."""
|
|
return os.environ.get("OSW_COMPOSE_ENTRY", "1") != "0"
|
|
|
|
|
|
@typechecked
|
|
def named_hosts(task: str, start_url: str) -> List[str]:
|
|
"""The hosts this task is about, in the order they should be trusted.
|
|
|
|
The site the USER NAMED wins, and where the card happens to sit is only the fallback for a task
|
|
that names none ("post this" while already on the site). That order is load-bearing for anything
|
|
that has to be ON a site before it can read from it: a cold run opens on a blank search page, so
|
|
trusting the card first made the first live discovery read google.com and correctly find
|
|
nothing. `compose_entry_for` can afford the opposite order because a table hit already proves
|
|
relevance; without a table there is nothing to filter a wrong host out.
|
|
|
|
Same extraction either way, kept in one place so the two cannot drift into disagreeing about
|
|
which site a task is for. The routing brief is excluded, so a model cannot redirect the post."""
|
|
out: List[str] = []
|
|
for candidate in (*[m.group(0) for m in P_URL_RE.finditer(human_words(task))],
|
|
*[m.group(1) for m in P_BARE_HOST_RE.finditer(human_words(task))],
|
|
start_url):
|
|
host = registrable_host(candidate)
|
|
if host and "." in host and host not in out:
|
|
out.append(host)
|
|
return out
|
|
|
|
|
|
@typechecked
|
|
def named_page(task: str, host: str) -> str:
|
|
"""The most specific page the user named on this host, else its front page.
|
|
|
|
Composing often lives under a section rather than at the root: "open a new issue" on
|
|
`github.com/owner/repo` is reachable from the repo and nowhere near `github.com/`, which
|
|
publishes no compose link at all. Measured, so the host alone is not enough to go on."""
|
|
want = registrable_host(host)
|
|
for m in P_URL_RE.finditer(human_words(task)):
|
|
url = m.group(0).rstrip(".,;)")
|
|
found = registrable_host(urlparse(url).netloc)
|
|
if found and (found == want or found.endswith("." + want)):
|
|
return url
|
|
return f"https://{want}/"
|
|
|
|
|
|
@typechecked
|
|
def compose_entry_for(task: str, start_url: str, task_is_send: bool) -> Optional[str]:
|
|
"""The URL to open to reach this site's composer, or None to leave navigation alone.
|
|
|
|
`start_url` is where the card already is; a host named in the task counts too, since a run that
|
|
begins on a blank tab still says "go to x.com and post ...".
|
|
|
|
`task_is_send` is the caller's already-computed write verdict and is REQUIRED, not defaulted,
|
|
because forgetting it is silently destructive: a quote is not proof of a write, and
|
|
`find the reddit post that says "..."` reads as a create to any regex short enough to be
|
|
readable (`post` is a noun there). Four such phrasings each resolved to reddit's SUBMIT page in
|
|
a probe, which would derail a plain read. The verdict the send script itself gates on is the
|
|
right authority, so this asks for it rather than growing a second opinion that can drift."""
|
|
if not enabled() or not task_is_send or not wants_top_level_compose(task):
|
|
return None
|
|
# Which site to open comes from the user's words; a brief naming some other site must not
|
|
# redirect the post.
|
|
asked = human_words(task)
|
|
named = [m.group(0) for m in P_URL_RE.finditer(asked)]
|
|
named += [m.group(1) for m in P_BARE_HOST_RE.finditer(asked)]
|
|
for candidate in (start_url, *named):
|
|
url = p_table_hit(candidate)
|
|
if not url:
|
|
continue
|
|
host = registrable_host(candidate)
|
|
if p_names_deeper_target(task, host):
|
|
return None
|
|
# Already on the compose surface: navigating again would remount it and throw away a
|
|
# composer that is right there.
|
|
if registrable_host(start_url) == host and p_on_compose_surface(start_url, url):
|
|
return None
|
|
return url
|
|
return None
|
|
|
|
|
|
@typechecked
|
|
def p_on_compose_surface(current_url: str, compose_url: str) -> bool:
|
|
"""True when the current URL is already this site's compose surface."""
|
|
cur, target = urlparse(current_url or ""), urlparse(compose_url or "")
|
|
if not cur.netloc:
|
|
return False
|
|
cur_mark = (cur.path.strip("/") + "?" + cur.query + "#" + cur.fragment).lower()
|
|
target_mark = (target.path.strip("/") + "?" + target.query + "#" + target.fragment).lower()
|
|
for token in ("compose", "submit", "shareactive"):
|
|
if token in target_mark and token in cur_mark:
|
|
return True
|
|
return False
|