notte-browser 0.0.dev0__py3-none-any.whl
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/__init__.py +3 -0
- notte_browser/controller.py +220 -0
- notte_browser/dom/__init__.py +0 -0
- notte_browser/dom/buildDomNode.js +516 -0
- notte_browser/dom/csspaths.py +155 -0
- notte_browser/dom/dropdown_menu.py +162 -0
- notte_browser/dom/id_generation.py +39 -0
- notte_browser/dom/locate.py +80 -0
- notte_browser/dom/parsing.py +158 -0
- notte_browser/dom/pipe.py +13 -0
- notte_browser/dom/types.py +465 -0
- notte_browser/dom/wait_for_page_update.py +132 -0
- notte_browser/errors.py +268 -0
- notte_browser/playwright.py +209 -0
- notte_browser/py.typed +0 -0
- notte_browser/rendering/__init__.py +0 -0
- notte_browser/rendering/interaction_only.py +125 -0
- notte_browser/rendering/json.py +47 -0
- notte_browser/rendering/markdown.py +71 -0
- notte_browser/rendering/pipe.py +87 -0
- notte_browser/rendering/pruning.py +124 -0
- notte_browser/resolution.py +118 -0
- notte_browser/scraping/__init__.py +0 -0
- notte_browser/scraping/llm_scraping.py +60 -0
- notte_browser/scraping/pipe.py +140 -0
- notte_browser/scraping/schema.py +145 -0
- notte_browser/scraping/simple.py +21 -0
- notte_browser/session.py +482 -0
- notte_browser/tagging/__init__.py +0 -0
- notte_browser/tagging/action/__init__.py +0 -0
- notte_browser/tagging/action/base.py +26 -0
- notte_browser/tagging/action/llm_taging/__init__.py +0 -0
- notte_browser/tagging/action/llm_taging/base.py +130 -0
- notte_browser/tagging/action/llm_taging/filtering.py +34 -0
- notte_browser/tagging/action/llm_taging/listing.py +146 -0
- notte_browser/tagging/action/llm_taging/parser.py +348 -0
- notte_browser/tagging/action/llm_taging/pipe.py +216 -0
- notte_browser/tagging/action/llm_taging/validation.py +40 -0
- notte_browser/tagging/action/pipe.py +75 -0
- notte_browser/tagging/action/simple/__init__.py +0 -0
- notte_browser/tagging/action/simple/pipe.py +71 -0
- notte_browser/tagging/page.py +35 -0
- notte_browser/vault.py +56 -0
- notte_browser/window.py +370 -0
- notte_browser-0.0.dev0.dist-info/METADATA +16 -0
- notte_browser-0.0.dev0.dist-info/RECORD +47 -0
- notte_browser-0.0.dev0.dist-info/WHEEL +4 -0
|
@@ -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
|