OCM-26400 / ocm26400 /browser_tool.py
akone's picture
Upload ocm26400/browser_tool.py with huggingface_hub
feac17e verified
Raw
History Blame Contribute Delete
5.94 kB
"""Browser interactif RÉEL (Playwright) — réfute audit H1.
L'audit H1 : « Browser use INTERACTIF (clics, JS, formulaires, login) manquant.
WebFetchTool = GET HTTP passif. Pas de Playwright/Selenium ». On comble avec un
vrai browser interactif via Playwright : naviguer, cliquer, remplir des formulaires,
extraire du texte, exécuter du JS. C'est la capacité 'browser use' du cahier des
charges, nécessaire pour BrowseComp / OSWorld.
SÉCURITÉ : SSRF protégé (réutilise _validate_url_safe), headless par défaut,
timeout, pas de téléchargement. Une session = un contexte browser isolé.
HONNÊTE : nécessite Playwright installé + browsers téléchargés
('playwright install chromium'). Si absent, available()=False et les méthodes
retournent un message clair (pas de crash). Le WebFetchTool passif reste le fallback.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from .web_tools import _validate_url_safe
try:
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
_HAS_PW = True
except ImportError:
_HAS_PW = False
class InteractiveBrowser:
"""Browser interactif (Playwright) : navigate, click, fill, extract, js.
Une instance gère un browser headless + une page. SSRF-safe."""
def __init__(self, headless: bool = True, timeout: int = 15000):
self.headless = headless
self.timeout = timeout
self._pw = None
self._browser = None
self._page = None
@staticmethod
def available() -> bool:
return _HAS_PW
def _ensure(self):
if not _HAS_PW:
raise RuntimeError("Playwright non installé (pip install playwright && "
"playwright install chromium)")
if self._page is None:
self._pw = sync_playwright().start()
self._browser = self._pw.chromium.launch(headless=self.headless)
self._page = self._page = self._browser.new_page()
self._page.set_default_timeout(self.timeout)
def navigate(self, url: str) -> Dict[str, Any]:
"""Navigue vers une URL (SSRF-validée). Retourne titre + status."""
url = _validate_url_safe(url)
self._ensure()
try:
resp = self._page.goto(url)
return {"url": url, "title": self._page.title(),
"status": resp.status if resp else None, "ok": True}
except PWTimeout:
return {"url": url, "ok": False, "error": "timeout"}
except Exception as e:
return {"url": url, "ok": False, "error": f"{type(e).__name__}: {e}"}
def click(self, selector: str) -> Dict[str, Any]:
"""Clique sur un élément (sélecteur CSS)."""
self._ensure()
try:
self._page.click(selector, timeout=self.timeout)
return {"selector": selector, "clicked": True}
except Exception as e:
return {"selector": selector, "clicked": False, "error": str(e)}
def fill(self, selector: str, value: str) -> Dict[str, Any]:
"""Remplit un champ de formulaire."""
self._ensure()
# validation : pas de sélecteur/value malveillant (Playwright échappe)
if len(value) > 10000:
return {"error": "value trop longue"}
try:
self._page.fill(selector, value, timeout=self.timeout)
return {"selector": selector, "filled": True}
except Exception as e:
return {"selector": selector, "filled": False, "error": str(e)}
def extract_text(self, selector: str = "body") -> Optional[str]:
"""Extrait le texte d'un élément (body par défaut)."""
self._ensure()
try:
return self._page.inner_text(selector)[:8000]
except Exception as e:
return f"[extract error: {type(e).__name__}]"
def extract_links(self) -> List[Dict[str, str]]:
"""Tous les liens de la page (texte + href)."""
self._ensure()
try:
return self._page.eval_on_selector_all(
"a", "els => els.map(e => ({text: e.innerText.trim().slice(0,80), href: e.href}))")
except Exception:
return []
def run_js(self, script: str) -> Any:
"""Exécute un script JS dans la page. SÉCURITÉ : réservé usage interne/audit."""
self._ensure()
try:
return self._page.evaluate(script)
except Exception as e:
return f"[js error: {type(e).__name__}]"
def screenshot(self, path: str) -> Dict[str, Any]:
self._ensure()
try:
self._page.screenshot(path=path)
return {"path": path, "ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
def close(self):
if self._browser:
self._browser.close()
if self._pw:
self._pw.stop()
self._browser = self._pw = self._page = None
def __enter__(self):
return self
def __exit__(self, *a):
self.close()
# ---------------- démo (best-effort, nécessite Playwright + chromium) ----------------
def demo() -> Dict[str, Any]:
"""Démo : navigue vers example.com, extrait titre + texte + liens."""
if not InteractiveBrowser.available():
return {"available": False,
"note": "Playwright non installé — pip install playwright && playwright install chromium"}
with InteractiveBrowser(headless=True) as b:
nav = b.navigate("https://example.com")
if not nav.get("ok"):
return {"available": True, "nav_failed": nav}
text = (b.extract_text() or "")[:200]
links = b.extract_links()[:5]
return {"available": True, "nav": nav, "text_sample": text,
"n_links": len(links), "links_sample": links}
if __name__ == "__main__":
import json
print(json.dumps(demo(), indent=2, default=str)[:1500])