]+class="result__snippet"[^>]*>|]+class="result__snippet"[^>]*>)(?P.*?)(?:a|div)>',
re.IGNORECASE | re.DOTALL,
)
for match in pattern.finditer(page):
title = self._clean_html(match.group("title"))
result_url = unescape(match.group("url"))
parsed = urllib.parse.urlparse(result_url)
if parsed.path.startswith("/l/"):
target = urllib.parse.parse_qs(parsed.query).get("uddg", [""])[0]
result_url = urllib.parse.unquote(target)
if title and result_url.startswith(("https://", "http://")):
results.append(SearchResult(title, result_url, self._clean_html(match.group("snippet"))))
if len(results) >= max(1, min(limit, 10)):
break
return results
@staticmethod
def _clean_html(value: str) -> str:
return " ".join(unescape(re.sub(r"<[^>]+>", " ", value)).split())
@dataclass(frozen=True, slots=True)
class WebPage:
url: str
title: str
text: str
class _ReadableTextParser(HTMLParser):
SKIP_TAGS = {"script", "style", "noscript", "svg", "nav", "footer", "header"}
def __init__(self) -> None:
super().__init__()
self.title = ""
self._in_title = False
self._skip_depth = 0
self.parts: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag in self.SKIP_TAGS:
self._skip_depth += 1
if tag == "title":
self._in_title = True
def handle_endtag(self, tag: str) -> None:
if tag in self.SKIP_TAGS and self._skip_depth:
self._skip_depth -= 1
if tag == "title":
self._in_title = False
def handle_data(self, data: str) -> None:
if self._skip_depth:
return
if self._in_title:
self.title += data
self.parts.append(data)
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, request, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
return None
class WebPageReader:
"""Fetch small readable extracts from explicitly requested public pages."""
MAX_BYTES = 1_500_000
MAX_REDIRECTS = 3
def read(self, url: str, *, timeout: float = 10.0, max_characters: int = 6_000) -> WebPage:
current_url = url
opener = urllib.request.build_opener(_NoRedirect())
for _ in range(self.MAX_REDIRECTS + 1):
self._validate_public_url(current_url)
request = urllib.request.Request(
current_url,
headers={"User-Agent": "ADAM-WebReader/1.0 (+local assistant)"},
)
try:
response = opener.open(request, timeout=timeout)
except urllib.error.HTTPError as exc:
if exc.code in {301, 302, 303, 307, 308} and exc.headers.get("Location"):
current_url = urllib.parse.urljoin(current_url, exc.headers["Location"])
continue
raise WebReadError(f"Could not read page (HTTP {exc.code}).") from exc
except (OSError, urllib.error.URLError, TimeoutError) as exc:
raise WebReadError(f"Could not read page: {exc}") from exc
with response:
content_type = response.headers.get_content_type()
if content_type not in {"text/html", "text/plain"}:
raise WebReadError(f"This page is {content_type}, not readable web text.")
raw = response.read(self.MAX_BYTES + 1)
if len(raw) > self.MAX_BYTES:
raise WebReadError("This page is too large to read safely.")
body = raw.decode(response.headers.get_content_charset() or "utf-8", errors="replace")
if content_type == "text/plain":
text = " ".join(body.split())
return WebPage(current_url, current_url, text[:max_characters])
parser = _ReadableTextParser()
parser.feed(body)
text = " ".join(parser.parts).strip()
title = " ".join(parser.title.split()) or current_url
return WebPage(current_url, title, text[:max_characters])
raise WebReadError("Page redirected too many times.")
@staticmethod
def _validate_public_url(url: str) -> None:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise WebReadError("Only public http(s) links can be read.")
try:
addresses = socket.getaddrinfo(parsed.hostname, None, type=socket.SOCK_STREAM)
except socket.gaierror as exc:
raise WebReadError("Could not resolve that website.") from exc
for item in addresses:
address = ipaddress.ip_address(item[4][0])
if not address.is_global:
raise WebReadError("Private, local, and reserved network addresses are blocked.")
def should_search(request: str) -> bool:
"""Whether a chat request explicitly asks ADAM for fresh web information."""
return bool(re.search(
r"\b(?:search(?:\s+the)?\s+(?:web|internet|online)|web\s+search|"
r"look\s+up|google|browse\s+(?:the\s+)?(?:web|internet)|"
r"latest|current|today'?s|recent\s+news)\b",
request,
re.IGNORECASE,
))
def search_query_from_request(request: str) -> str:
"""Remove a conversational search command before sending the actual topic."""
query = re.sub(
r"^\s*(?:please\s+)?(?:search(?:\s+the)?\s+(?:web|internet|online)|"
r"web\s+search|look\s+up|google|browse\s+(?:the\s+)?(?:web|internet))"
r"\s+(?:for\s+)?",
"",
request,
flags=re.IGNORECASE,
)
return query.strip(" \t\r\n?!.") or request
def should_read_links(request: str) -> bool:
return bool(re.search(
r"\b(?:read|open|research|study|summari[sz]e|go\s+further|learn\s+more|"
r"look\s+into)\b.*\b(?:link|links|result|results|page|pages|web|website|site)\b"
r"|\b(?:read|open)\s+https?://",
request,
re.IGNORECASE,
))
def urls_in_request(request: str) -> list[str]:
return re.findall(r"https?://[^\s<>\]\[\)\}\"']+", request)
def format_search_context(results: list[SearchResult]) -> str:
if not results:
return "No usable web results were returned. Be transparent about that."
lines = []
for index, result in enumerate(results, 1):
snippet = f" — {result.snippet}" if result.snippet else ""
lines.append(f"[{index}] {result.title}{snippet}\nURL: {result.url}")
return "\n".join(lines)
def format_page_context(pages: list[WebPage]) -> str:
if not pages:
return "No linked pages could be read."
return "\n\n".join(
f"PAGE: {page.title}\nURL: {page.url}\nEXTRACT: {page.text}"
for page in pages
)