-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools.py
More file actions
148 lines (120 loc) · 6.43 KB
/
Copy pathtools.py
File metadata and controls
148 lines (120 loc) · 6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""The browser tools — thin subprocess wrappers over the `agent-browser` CLI.
agent-browser (vercel-labs) is a native-Rust CLI + daemon: each invocation talks to
a persistent browser session, so these tools are stateless shells. The model's loop
is **open → snapshot → act on an `@eN` ref → verify**; `snapshot` returns the
accessibility tree with the refs the other tools consume.
Tools return the CLI's stdout (refs, extracted text, file paths) for the model to
read, and degrade to a readable ``Error: …`` string rather than raising — a failed
browser action should inform the loop, not crash it.
"""
from __future__ import annotations
import asyncio
import logging
import subprocess
from langchain_core.tools import tool
from .runtime import launch_flags
log = logging.getLogger("protoagent.plugins.agent_browser")
def get_browser_tools(cfg: dict | None):
cfg = cfg or {}
binary = str(cfg.get("binary") or "agent-browser")
timeout = float(cfg.get("timeout_s", 60))
def _run(*args: str) -> str:
"""Run `agent-browser <args>` and return stdout, or a readable error."""
try:
proc = subprocess.run([binary, *args], capture_output=True, text=True, timeout=timeout)
except FileNotFoundError:
return (f"Error: {binary!r} not on PATH — install it: "
"`npm i -g agent-browser && agent-browser install`")
except subprocess.TimeoutExpired:
return f"Error: `agent-browser {' '.join(args)}` timed out after {timeout:g}s"
out = (proc.stdout or "").strip()
if proc.returncode != 0:
err = (proc.stderr or out or "").strip()
return f"Error: `agent-browser {' '.join(args)}` failed: {err[:500]}"
return out or "(ok)"
async def _ab(*args: str) -> str:
return await asyncio.to_thread(_run, *args)
# ── navigation ────────────────────────────────────────────────────────────
@tool
async def browser_open(url: str = "") -> str:
"""Launch the browser (or navigate the current session). Pass a `url` to go
there, or leave blank to open about:blank. Start every browsing task here.
The configured runtime options (headed/profile/device/allowed_domains/stealth/…)
are applied here, where the session launches."""
flags = launch_flags(cfg)
return await _ab(*flags, "open", url) if url else await _ab(*flags, "open")
@tool
async def browser_back() -> str:
"""Navigate back in history."""
return await _ab("back")
@tool
async def browser_forward() -> str:
"""Navigate forward in history."""
return await _ab("forward")
@tool
async def browser_reload() -> str:
"""Reload the current page."""
return await _ab("reload")
# ── perception ────────────────────────────────────────────────────────────
@tool
async def browser_snapshot() -> str:
"""Get the page's accessibility tree with compact `@eN` element refs. Call
this before clicking/filling — the refs (e.g. `@e2`) are what the action
tools target. The canonical way to 'see' the page for the agent."""
return await _ab("snapshot")
@tool
async def browser_get_text(selector: str = "body") -> str:
"""Get the visible text of an element (a `@eN` ref or a CSS selector;
defaults to the whole `body`). Use to extract/read page content."""
return await _ab("get", "text", selector)
@tool
async def browser_get_html(selector: str = "") -> str:
"""Get the HTML of an element (a `@eN` ref or CSS selector), or the page."""
return await _ab("get", "html", selector) if selector else await _ab("get", "html")
@tool
async def browser_get_value(selector: str) -> str:
"""Get the current value of a form field (a `@eN` ref or CSS selector)."""
return await _ab("get", "value", selector)
# ── interaction ───────────────────────────────────────────────────────────
@tool
async def browser_click(selector: str) -> str:
"""Click an element by `@eN` ref (from `browser_snapshot`) or a CSS selector
(e.g. `#submit`). Snapshot first to get the ref."""
return await _ab("click", selector)
@tool
async def browser_fill(selector: str, text: str) -> str:
"""Clear a field and fill it with `text` (a `@eN` ref or CSS selector)."""
return await _ab("fill", selector, text)
@tool
async def browser_type(selector: str, text: str) -> str:
"""Type `text` into an element without clearing it first (a ref or selector)."""
return await _ab("type", selector, text)
@tool
async def browser_press(key: str) -> str:
"""Press a key or chord on the focused element (e.g. `Enter`, `Tab`,
`Control+a`)."""
return await _ab("press", key)
@tool
async def browser_hover(selector: str) -> str:
"""Hover the pointer over an element (a `@eN` ref or CSS selector)."""
return await _ab("hover", selector)
@tool
async def browser_eval(expression: str) -> str:
"""Evaluate a JavaScript `expression` in the page and return the result.
Use sparingly — prefer snapshot + the action tools."""
return await _ab("eval", expression)
# ── capture + session ─────────────────────────────────────────────────────
@tool
async def browser_screenshot(path: str = "page.png") -> str:
"""Save a screenshot of the current page to `path`. Returns the file path."""
return await _ab("screenshot", path)
@tool
async def browser_close() -> str:
"""Close the browser session. Call when the task is done to free the daemon."""
return await _ab("close")
return [
browser_open, browser_back, browser_forward, browser_reload,
browser_snapshot, browser_get_text, browser_get_html, browser_get_value,
browser_click, browser_fill, browser_type, browser_press, browser_hover,
browser_eval, browser_screenshot, browser_close,
]