notte-browser 0.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notte_browser-0.0.dev0/.gitignore +179 -0
- notte_browser-0.0.dev0/PKG-INFO +16 -0
- notte_browser-0.0.dev0/README.md +5 -0
- notte_browser-0.0.dev0/pyproject.toml +26 -0
- notte_browser-0.0.dev0/src/notte_browser/__init__.py +3 -0
- notte_browser-0.0.dev0/src/notte_browser/controller.py +220 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/buildDomNode.js +516 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/csspaths.py +155 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/dropdown_menu.py +162 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/id_generation.py +39 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/locate.py +80 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/parsing.py +158 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/pipe.py +13 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/types.py +465 -0
- notte_browser-0.0.dev0/src/notte_browser/dom/wait_for_page_update.py +132 -0
- notte_browser-0.0.dev0/src/notte_browser/errors.py +268 -0
- notte_browser-0.0.dev0/src/notte_browser/playwright.py +209 -0
- notte_browser-0.0.dev0/src/notte_browser/py.typed +0 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/interaction_only.py +125 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/json.py +47 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/markdown.py +71 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/pipe.py +87 -0
- notte_browser-0.0.dev0/src/notte_browser/rendering/pruning.py +124 -0
- notte_browser-0.0.dev0/src/notte_browser/resolution.py +118 -0
- notte_browser-0.0.dev0/src/notte_browser/scraping/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/scraping/llm_scraping.py +60 -0
- notte_browser-0.0.dev0/src/notte_browser/scraping/pipe.py +140 -0
- notte_browser-0.0.dev0/src/notte_browser/scraping/schema.py +145 -0
- notte_browser-0.0.dev0/src/notte_browser/scraping/simple.py +21 -0
- notte_browser-0.0.dev0/src/notte_browser/session.py +482 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/base.py +26 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/base.py +130 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/filtering.py +34 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/listing.py +146 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/parser.py +348 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/pipe.py +216 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/llm_taging/validation.py +40 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/pipe.py +75 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/simple/__init__.py +0 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/action/simple/pipe.py +71 -0
- notte_browser-0.0.dev0/src/notte_browser/tagging/page.py +35 -0
- notte_browser-0.0.dev0/src/notte_browser/vault.py +56 -0
- notte_browser-0.0.dev0/src/notte_browser/window.py +370 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
ignore.*
|
|
171
|
+
llm_usage.jsonl
|
|
172
|
+
llm_parsing_error.jsonl
|
|
173
|
+
traces/
|
|
174
|
+
|
|
175
|
+
**/__pycache__/**
|
|
176
|
+
.DS_Store
|
|
177
|
+
**/.DS_Store
|
|
178
|
+
old
|
|
179
|
+
notebook
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notte-browser
|
|
3
|
+
Version: 0.0.dev0
|
|
4
|
+
Summary: The web browser for LLMs agents
|
|
5
|
+
Author-email: Notte Team <hello@notte.cc>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: maincontentextractor
|
|
8
|
+
Requires-Dist: notte-core==0.0.dev
|
|
9
|
+
Requires-Dist: patchright==1.50.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Notte Processing Pipelines
|
|
13
|
+
|
|
14
|
+
Each pipeline is a set of steps that are used to preprocess either a browser snapshot or a LLM observation.
|
|
15
|
+
|
|
16
|
+
Each file in this directory is a separate but mandatory in the global workflow.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "notte-browser"
|
|
3
|
+
version = "0.0.dev"
|
|
4
|
+
description = "The web browser for LLMs agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Notte Team ", email = "hello@notte.cc" }
|
|
8
|
+
]
|
|
9
|
+
packages = [
|
|
10
|
+
{ include = "notte_browser", from = "src" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"notte_core==0.0.dev",
|
|
17
|
+
"patchright==1.50.0",
|
|
18
|
+
"maincontentextractor",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
maincontentextractor = { git = "https://github.com/HawkClaws/main_content_extractor", rev = "7c3ed7f6ed7f6c10223a3357d43ab741663bc812" }
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from notte_core.browser.snapshot import BrowserSnapshot
|
|
3
|
+
from notte_core.controller.actions import (
|
|
4
|
+
BaseAction,
|
|
5
|
+
CheckAction,
|
|
6
|
+
ClickAction,
|
|
7
|
+
CompletionAction,
|
|
8
|
+
FillAction,
|
|
9
|
+
GoBackAction,
|
|
10
|
+
GoForwardAction,
|
|
11
|
+
GotoAction,
|
|
12
|
+
GotoNewTabAction,
|
|
13
|
+
InteractionAction,
|
|
14
|
+
ListDropdownOptionsAction,
|
|
15
|
+
PressKeyAction,
|
|
16
|
+
ReloadAction,
|
|
17
|
+
ScrapeAction,
|
|
18
|
+
ScrollDownAction,
|
|
19
|
+
ScrollUpAction,
|
|
20
|
+
SelectDropdownOptionAction,
|
|
21
|
+
SwitchTabAction,
|
|
22
|
+
WaitAction,
|
|
23
|
+
)
|
|
24
|
+
from notte_core.credentials.types import get_str_value
|
|
25
|
+
from notte_core.utils.code import text_contains_tabs
|
|
26
|
+
from notte_core.utils.platform import platform_control_key
|
|
27
|
+
from patchright.async_api import Locator
|
|
28
|
+
from typing_extensions import final
|
|
29
|
+
|
|
30
|
+
from notte_browser.dom.dropdown_menu import dropdown_menu_options
|
|
31
|
+
from notte_browser.dom.locate import locate_element
|
|
32
|
+
from notte_browser.errors import capture_playwright_errors
|
|
33
|
+
from notte_browser.window import BrowserWindow
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@final
|
|
37
|
+
class BrowserController:
|
|
38
|
+
def __init__(self, verbose: bool = False) -> None:
|
|
39
|
+
self.verbose: bool = verbose
|
|
40
|
+
|
|
41
|
+
self.execute = capture_playwright_errors(verbose=verbose)(self.execute) # type: ignore[reportAttributeAccessIssue]
|
|
42
|
+
|
|
43
|
+
async def switch_tab(self, window: BrowserWindow, tab_index: int) -> None:
|
|
44
|
+
context = window.page.context
|
|
45
|
+
if tab_index != -1 and (tab_index < 0 or tab_index >= len(context.pages)):
|
|
46
|
+
raise ValueError(f"Tab index '{tab_index}' is out of range for context with {len(context.pages)} pages")
|
|
47
|
+
tab_page = context.pages[tab_index]
|
|
48
|
+
await tab_page.bring_to_front()
|
|
49
|
+
window.page = tab_page
|
|
50
|
+
await window.long_wait()
|
|
51
|
+
if self.verbose:
|
|
52
|
+
logger.info(
|
|
53
|
+
f"🪦 Switched to tab {tab_index} with url: {tab_page.url} ({len(context.pages)} tabs in context)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def execute_browser_action(self, window: BrowserWindow, action: BaseAction) -> BrowserSnapshot | None:
|
|
57
|
+
match action:
|
|
58
|
+
case GotoAction(url=url):
|
|
59
|
+
return await window.goto(url)
|
|
60
|
+
case GotoNewTabAction(url=url):
|
|
61
|
+
new_page = await window.page.context.new_page()
|
|
62
|
+
window.page = new_page
|
|
63
|
+
_ = await new_page.goto(url)
|
|
64
|
+
case SwitchTabAction(tab_index=tab_index):
|
|
65
|
+
await self.switch_tab(window, tab_index)
|
|
66
|
+
case WaitAction(time_ms=time_ms):
|
|
67
|
+
await window.page.wait_for_timeout(time_ms)
|
|
68
|
+
case GoBackAction():
|
|
69
|
+
_ = await window.page.go_back()
|
|
70
|
+
case GoForwardAction():
|
|
71
|
+
_ = await window.page.go_forward()
|
|
72
|
+
case ReloadAction():
|
|
73
|
+
_ = await window.page.reload()
|
|
74
|
+
await window.long_wait()
|
|
75
|
+
case PressKeyAction(key=key):
|
|
76
|
+
await window.page.keyboard.press(key)
|
|
77
|
+
case ScrollUpAction(amount=amount):
|
|
78
|
+
if amount is not None:
|
|
79
|
+
await window.page.mouse.wheel(delta_x=0, delta_y=-amount)
|
|
80
|
+
else:
|
|
81
|
+
await window.page.keyboard.press("PageUp")
|
|
82
|
+
case ScrollDownAction(amount=amount):
|
|
83
|
+
if amount is not None:
|
|
84
|
+
await window.page.mouse.wheel(delta_x=0, delta_y=amount)
|
|
85
|
+
else:
|
|
86
|
+
await window.page.keyboard.press("PageDown")
|
|
87
|
+
case ScrapeAction():
|
|
88
|
+
raise NotImplementedError("Scrape action is not supported in the browser controller")
|
|
89
|
+
case _:
|
|
90
|
+
raise ValueError(f"Unsupported action type: {type(action)}")
|
|
91
|
+
|
|
92
|
+
# perform snapshot in execute
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def execute_interaction_action(
|
|
96
|
+
self, window: BrowserWindow, action: InteractionAction
|
|
97
|
+
) -> BrowserSnapshot | None:
|
|
98
|
+
if action.selector is None:
|
|
99
|
+
raise ValueError(f"Selector is required for {action.name()}")
|
|
100
|
+
press_enter = False
|
|
101
|
+
if action.press_enter is not None:
|
|
102
|
+
press_enter = action.press_enter
|
|
103
|
+
# locate element (possibly in iframe)
|
|
104
|
+
locator: Locator = await locate_element(window.page, action.selector)
|
|
105
|
+
original_url = window.page.url
|
|
106
|
+
|
|
107
|
+
action_timeout = window.config.wait.action_timeout
|
|
108
|
+
|
|
109
|
+
match action:
|
|
110
|
+
# Interaction actions
|
|
111
|
+
case ClickAction():
|
|
112
|
+
await locator.click(timeout=action_timeout)
|
|
113
|
+
case FillAction(value=value):
|
|
114
|
+
if text_contains_tabs(text=get_str_value(value)):
|
|
115
|
+
if self.verbose:
|
|
116
|
+
logger.info(
|
|
117
|
+
"🪦 Indentation detected in fill action: simulating clipboard copy/paste for better string formatting"
|
|
118
|
+
)
|
|
119
|
+
await locator.focus()
|
|
120
|
+
|
|
121
|
+
if action.clear_before_fill:
|
|
122
|
+
await window.page.keyboard.press(key=f"{platform_control_key()}+A")
|
|
123
|
+
await window.short_wait()
|
|
124
|
+
await window.page.keyboard.press(key="Backspace")
|
|
125
|
+
await window.short_wait()
|
|
126
|
+
|
|
127
|
+
# Use isolated clipboard variable instead of system clipboard
|
|
128
|
+
await window.page.evaluate(
|
|
129
|
+
"""
|
|
130
|
+
(text) => {
|
|
131
|
+
window.__isolatedClipboard = text;
|
|
132
|
+
const dataTransfer = new DataTransfer();
|
|
133
|
+
dataTransfer.setData('text/plain', window.__isolatedClipboard);
|
|
134
|
+
document.activeElement.dispatchEvent(new ClipboardEvent('paste', {
|
|
135
|
+
clipboardData: dataTransfer,
|
|
136
|
+
bubbles: true,
|
|
137
|
+
cancelable: true
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
""",
|
|
141
|
+
value,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
await window.short_wait()
|
|
145
|
+
else:
|
|
146
|
+
await locator.fill(get_str_value(value), timeout=action_timeout, force=action.clear_before_fill)
|
|
147
|
+
await window.short_wait()
|
|
148
|
+
case CheckAction(value=value):
|
|
149
|
+
if value:
|
|
150
|
+
await locator.check()
|
|
151
|
+
else:
|
|
152
|
+
await locator.uncheck()
|
|
153
|
+
case SelectDropdownOptionAction(value=value, option_selector=option_selector):
|
|
154
|
+
# Check if it's a standard HTML select
|
|
155
|
+
tag_name: str = await locator.evaluate("el => el.tagName.toLowerCase()")
|
|
156
|
+
if tag_name == "select":
|
|
157
|
+
# Handle standard HTML select
|
|
158
|
+
_ = await locator.select_option(value)
|
|
159
|
+
elif option_selector is None:
|
|
160
|
+
raise ValueError(f"Option selector is required for {action.name()}")
|
|
161
|
+
else:
|
|
162
|
+
option_locator = await locate_element(window.page, option_selector)
|
|
163
|
+
# Handle non-standard select
|
|
164
|
+
await option_locator.click()
|
|
165
|
+
|
|
166
|
+
case ListDropdownOptionsAction():
|
|
167
|
+
options = await dropdown_menu_options(window.page, action.selector.xpath_selector)
|
|
168
|
+
if self.verbose:
|
|
169
|
+
logger.info(f"Dropdown options: {options}")
|
|
170
|
+
raise NotImplementedError("ListDropdownOptionsAction is not supported in the browser controller")
|
|
171
|
+
case _:
|
|
172
|
+
raise ValueError(f"Unsupported action type: {type(action)}")
|
|
173
|
+
if press_enter:
|
|
174
|
+
if self.verbose:
|
|
175
|
+
logger.info(f"🪦 Pressing enter for action {action.id}")
|
|
176
|
+
await window.short_wait()
|
|
177
|
+
await window.page.keyboard.press("Enter")
|
|
178
|
+
if original_url != window.page.url:
|
|
179
|
+
if self.verbose:
|
|
180
|
+
logger.info(f"🪦 Page navigation detected for action {action.id} waiting for networkidle")
|
|
181
|
+
await window.long_wait()
|
|
182
|
+
|
|
183
|
+
# perform snapshot in execute
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
async def execute(self, window: BrowserWindow, action: BaseAction) -> BrowserSnapshot:
|
|
187
|
+
context = window.page.context
|
|
188
|
+
num_pages = len(context.pages)
|
|
189
|
+
match action:
|
|
190
|
+
case InteractionAction():
|
|
191
|
+
retval = await self.execute_interaction_action(window, action)
|
|
192
|
+
case CompletionAction(success=success, answer=answer):
|
|
193
|
+
snapshot = await window.snapshot()
|
|
194
|
+
if self.verbose:
|
|
195
|
+
logger.info(
|
|
196
|
+
f"Completion action: status={'success' if success else 'failure'} with answer = {answer}"
|
|
197
|
+
)
|
|
198
|
+
# await window.close()
|
|
199
|
+
return snapshot
|
|
200
|
+
case _:
|
|
201
|
+
retval = await self.execute_browser_action(window, action)
|
|
202
|
+
# add short wait before we check for new tabs to make sure that
|
|
203
|
+
# the page has time to be created
|
|
204
|
+
await window.short_wait()
|
|
205
|
+
if len(context.pages) != num_pages:
|
|
206
|
+
if self.verbose:
|
|
207
|
+
logger.info(f"🪦 Action {action.id} resulted in a new tab, switched to it...")
|
|
208
|
+
await self.switch_tab(window, -1)
|
|
209
|
+
elif retval is not None:
|
|
210
|
+
# only return snapshot if we didn't switch to a new tab
|
|
211
|
+
# otherwise, the snapshot is out of date and we need to take a new one
|
|
212
|
+
return retval
|
|
213
|
+
|
|
214
|
+
return await window.snapshot()
|
|
215
|
+
|
|
216
|
+
async def execute_multiple(self, window: BrowserWindow, actions: list[BaseAction]) -> list[BrowserSnapshot]:
|
|
217
|
+
snapshots: list[BrowserSnapshot] = []
|
|
218
|
+
for action in actions:
|
|
219
|
+
snapshots.append(await self.execute(window, action))
|
|
220
|
+
return snapshots
|
|
File without changes
|