WebNavigatorAgent — Catalogued Web Navigation on Your Chrome Profile¶
Overview¶
WebNavigatorAgent is an Agent that operates websites through the
action catalog of WebBrowsingToolkit (parrot_tools.browsing). The
LLM never improvises selectors or navigation steps: from a natural-language
request it prepares one structured WebTaskRequest
({site, action, data}) and calls execute_web_task, which replays a
pre-recorded script deterministically over a persistent browser session.
Because the toolkit can be pointed at a real Google Chrome user-data directory, every catalogued action runs with your session cookies, saved logins and OS keyring — ideal for repeatable flows on sites where you are already authenticated.
The reference implementation lives in
examples/agents/webbrowser/webbrowsing_agent.py; the sample catalog
(targeting the public sandbox quotes.toscrape.com)
lives in examples/webbrowsing/.
Compare with WebAgent, which lets the LLM drive Chrome freely through the Chrome DevTools Protocol.
Architecture¶
"dame las citas del tag love"
│
▼
WebNavigatorAgent (Agent + system prompt)
│ WebTaskRequest {site: "quotes", action: "quotes-by-tag", data: {tag: "love"}}
▼
WebBrowsingToolkit.execute_web_task
│ resolves site → loads catalog/<site>/<action>.json
│ injects prerequisites (requires: [login]) once per sequence
▼
Playwright / Selenium driver (fixed at construction)
│ user_data_dir + profile_directory + browser_channel="chrome"
▼
Google Chrome (your profile)
The agent¶
from parrot.bots.agent import Agent
from parrot_tools.browsing import WebBrowsingToolkit
class WebNavigatorAgent(Agent):
def __init__(
self,
user_data_dir=None,
profile_directory="Default",
catalog_dir="examples/webbrowsing/catalog",
driver_type="playwright",
headless=True,
**kwargs,
):
self.browsing_toolkit = WebBrowsingToolkit(
catalog_dir=catalog_dir,
driver_type=driver_type,
browser="chrome",
headless=headless,
user_data_dir=user_data_dir,
profile_directory=profile_directory if user_data_dir else None,
# Launch the system Chrome so keyring-encrypted data is readable
browser_channel="chrome" if user_data_dir else None,
confirm_runs=False, # demo; keep True for production sites
)
super().__init__(
name="WebNavigator",
tools=[self.browsing_toolkit],
system_prompt=SYSTEM_PROMPT,
**kwargs,
)
async def close(self):
await self.browsing_toolkit.close_browser()
| Constructor arg | Description |
|---|---|
user_data_dir |
Chrome user-data directory (~/.config/google-chrome, ~/Library/Application Support/Google/Chrome, %LOCALAPPDATA%\Google\Chrome\User Data). None → clean, profile-less browser. |
profile_directory |
Profile folder inside user_data_dir (Default, Profile 1, …). |
catalog_dir |
Catalog root: one folder per site, one JSON per action. |
driver_type |
"playwright" or "selenium" — fixed for the agent's lifetime. |
headless |
Run without a window. |
**kwargs |
Forwarded to Agent (llm, use_llm, …). |
The system prompt tells the model to build one WebTaskRequest, to repair
it from the structured error hints when the toolkit returns
status="error", and to use list_sites / list_site_actions when
unsure — never to invent steps.
Running the example¶
source .venv/bin/activate
uv pip install ai-parrot-tools playwright
playwright install chromium
# Clean browser, headless
python examples/agents/webbrowser/webbrowsing_agent.py "dame las citas del tag love"
# Visible window
HEADLESS=0 python examples/agents/webbrowser/webbrowsing_agent.py "inicia sesión en quotes"
# Against your real Chrome profile
CHROME_USER_DATA=~/.config/google-chrome CHROME_PROFILE="Profile 1" HEADLESS=0 \
python examples/agents/webbrowser/webbrowsing_agent.py "inicia sesión en quotes y lista la portada"
| Env var | Default | Purpose |
|---|---|---|
CHROME_USER_DATA |
— | User-data directory; unset → profile-less browser. |
CHROME_PROFILE |
Default |
Profile folder inside CHROME_USER_DATA. |
HEADLESS |
1 |
0 shows the browser. |
Chrome locks a profile in use
Close Chrome before running with CHROME_USER_DATA, or point the agent
at a copy of the directory (next section).
Programmatic use:
agent = WebNavigatorAgent(
user_data_dir="~/.config/google-chrome",
profile_directory="Default",
driver_type="playwright",
headless=False,
llm="google:gemini-2.5-flash",
)
await agent.configure()
answer, _ = await agent.invoke("dame las citas del tag love")
await agent.close()
Copying your Chrome profile¶
Working on a copy avoids the profile lock and protects your real profile from anything the agent does. Quit Chrome first (otherwise the SQLite cookie/history databases may be copied mid-write) and skip the caches.
SRC="$HOME/.config/google-chrome" # chromium: ~/.config/chromium
DST="$HOME/.config/chrome-debug"
pkill -x chrome 2>/dev/null; sleep 2
rsync -a --delete \
--exclude='Singleton*' \
--exclude='*/Cache/' --exclude='*/Code Cache/' --exclude='*/GPUCache/' \
--exclude='*/Service Worker/CacheStorage/' --exclude='GrShaderCache/' \
"$SRC/" "$DST/"
SRC="$HOME/Library/Application Support/Google/Chrome"
DST="$HOME/chrome-debug"
osascript -e 'quit app "Google Chrome"'; sleep 2
rsync -a --delete \
--exclude='Singleton*' \
--exclude='*/Cache/' --exclude='*/Code Cache/' --exclude='*/GPUCache/' \
--exclude='*/Service Worker/CacheStorage/' --exclude='GrShaderCache/' \
"$SRC/" "$DST/"
Singleton*(SingletonLock,SingletonSocket,SingletonCookie) are Chrome's lock files — never copy them; delete them from$DSTif Chrome claims the copy is already in use.- Find which
Profile Nis which inchrome://version(Profile Path) orjq '.profile.info_cache' "$SRC/Local State". - Cookies and passwords are encrypted with the OS keyring
(
libsecret/ Keychain), bound to the user, not the path: the copy stays readable on the same machine and account, not on another host. macOS may prompt for "Chrome Safe Storage" access the first time. - Re-run the
rsyncto refresh the copy with new sessions.
Use $DST both as CHROME_USER_DATA here and as --user-data-dir when
starting Chrome with remote debugging
for WebAgent.
The action catalog¶
The catalog is plain JSON on disk — catalog/<site>/_site.json plus one
file per action. Scripts use the same BrowserAction DSL as
WebScrapingToolkit (navigate, click, fill, extract, loop, …)
with {{param}} placeholders.
{
"site": "quotes-toscrape-com",
"base_url": "https://quotes.toscrape.com",
"title": "Quotes to Scrape",
"aliases": ["quotes", "toscrape"]
}
{
"name": "quotes-by-tag",
"kind": "operation",
"params": {"tag": {"description": "Tag slug to browse", "required": true, "example": "love"}},
"steps": [
{"action": "navigate", "url": "https://quotes.toscrape.com/tag/{{tag}}/"},
{"action": "extract", "selector": "div.quote", "multiple": true,
"extract_name": "quotes",
"fields": {"text": {"selector": "span.text"},
"author": {"selector": "small.author"},
"tags": {"selector": "div.tags a.tag", "multiple": true}}}
]
}
Composite actions chain other actions and declare prerequisites that are injected once per sequence (a login runs a single time even if several composed actions require it):
{
"name": "login-and-list",
"kind": "composite",
"requires": ["login"],
"compose": [{"action": "list-quotes", "params": {}}],
"params": {"username": {"required": true, "default": "parrot"},
"password": {"required": true, "default": "parrot"}}
}
Regenerate the sample catalog with python examples/webbrowsing/seed_catalog.py;
examples/webbrowsing/local_demo.py replays it offline (no LLM, no
network) and doubles as a smoke test.
The structured contract (execute_web_task)¶
Multi-step flows use plan instead of action:
{"site": "quotes", "data": {"username": "parrot", "password": "parrot"},
"plan": [{"action": "login"}, {"action": "top-tags"}]}
Failures come back structured and repairable instead of raising, so the LLM self-corrects in one round-trip:
error.code |
Hint returned |
|---|---|
unknown_site |
known_sites (slug, title, aliases) |
unknown_action |
available_actions (name, description, params) |
missing_params |
expected_params per action + provided_data |
invalid_request / invalid_plan |
validation message |
WebTaskRequest is a Pydantic model, so it can also be used directly as a
structured-output schema.
Toolkit tools¶
| Tool | Purpose |
|---|---|
execute_web_task |
The one-shot structured entry point described above. |
list_sites, list_site_actions, get_site_action |
Catalog discovery. |
run_site_action, run_site_sequence |
Run one action / an explicit sequence (gated by confirm_runs). |
register_site, save_site_action, delete_site_action |
Catalog authoring. save_site_action rejects literal credentials — use an authenticate step with credential_provider + a credential_resolver (CredentialBroker). |
close_browser |
End the persistent session. |
WebBrowsingToolkit options¶
| Arg | Default | Notes |
|---|---|---|
catalog_dir |
"browsing_catalog" |
Catalog root. |
driver_type |
— | "playwright" / "selenium", fixed at construction. |
user_data_dir, profile_directory, browser_channel |
None |
Real Chrome profile; browser_channel="chrome" launches system Chrome. |
session_based |
True |
One browser instance across every run until close_browser(). |
headless |
False |
Catalogued flows are visible by default. |
confirm_runs |
True |
Marks run_site_action / run_site_sequence as HITL-confirmed tools. |
max_loop_iterations |
toolkit default | Hard cap on every loop step (save time + run time). |
credential_resolver, human_channel |
None |
Credential broker and await_human channel. |
Choosing between the two web agents¶
WebNavigatorAgent |
WebAgent |
|
|---|---|---|
| Browser control | Playwright/Selenium launch Chrome | Attaches to a running Chrome via CDP |
| What the LLM does | Picks a catalogued action (deterministic replay) | Navigates freely with DevTools tools |
| Profile | user_data_dir + profile_directory in the constructor |
--user-data-dir when starting Chrome |
| Extra deps | playwright or selenium |
Node / npx (chrome-devtools-mcp) |
| Best for | Repeatable flows on known sites | Exploration, QA suites, debugging (console, network, screenshots) |
See also¶
examples/agents/webbrowser/README.md— runnable walkthrough (ES).examples/webbrowsing/README.md— catalog reference and offline demo.- WebAgent
- Tools Reference