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.
Files changed (47) hide show
  1. notte_browser/__init__.py +3 -0
  2. notte_browser/controller.py +220 -0
  3. notte_browser/dom/__init__.py +0 -0
  4. notte_browser/dom/buildDomNode.js +516 -0
  5. notte_browser/dom/csspaths.py +155 -0
  6. notte_browser/dom/dropdown_menu.py +162 -0
  7. notte_browser/dom/id_generation.py +39 -0
  8. notte_browser/dom/locate.py +80 -0
  9. notte_browser/dom/parsing.py +158 -0
  10. notte_browser/dom/pipe.py +13 -0
  11. notte_browser/dom/types.py +465 -0
  12. notte_browser/dom/wait_for_page_update.py +132 -0
  13. notte_browser/errors.py +268 -0
  14. notte_browser/playwright.py +209 -0
  15. notte_browser/py.typed +0 -0
  16. notte_browser/rendering/__init__.py +0 -0
  17. notte_browser/rendering/interaction_only.py +125 -0
  18. notte_browser/rendering/json.py +47 -0
  19. notte_browser/rendering/markdown.py +71 -0
  20. notte_browser/rendering/pipe.py +87 -0
  21. notte_browser/rendering/pruning.py +124 -0
  22. notte_browser/resolution.py +118 -0
  23. notte_browser/scraping/__init__.py +0 -0
  24. notte_browser/scraping/llm_scraping.py +60 -0
  25. notte_browser/scraping/pipe.py +140 -0
  26. notte_browser/scraping/schema.py +145 -0
  27. notte_browser/scraping/simple.py +21 -0
  28. notte_browser/session.py +482 -0
  29. notte_browser/tagging/__init__.py +0 -0
  30. notte_browser/tagging/action/__init__.py +0 -0
  31. notte_browser/tagging/action/base.py +26 -0
  32. notte_browser/tagging/action/llm_taging/__init__.py +0 -0
  33. notte_browser/tagging/action/llm_taging/base.py +130 -0
  34. notte_browser/tagging/action/llm_taging/filtering.py +34 -0
  35. notte_browser/tagging/action/llm_taging/listing.py +146 -0
  36. notte_browser/tagging/action/llm_taging/parser.py +348 -0
  37. notte_browser/tagging/action/llm_taging/pipe.py +216 -0
  38. notte_browser/tagging/action/llm_taging/validation.py +40 -0
  39. notte_browser/tagging/action/pipe.py +75 -0
  40. notte_browser/tagging/action/simple/__init__.py +0 -0
  41. notte_browser/tagging/action/simple/pipe.py +71 -0
  42. notte_browser/tagging/page.py +35 -0
  43. notte_browser/vault.py +56 -0
  44. notte_browser/window.py +370 -0
  45. notte_browser-0.0.dev0.dist-info/METADATA +16 -0
  46. notte_browser-0.0.dev0.dist-info/RECORD +47 -0
  47. notte_browser-0.0.dev0.dist-info/WHEEL +4 -0
@@ -0,0 +1,3 @@
1
+ from notte_core import check_notte_version
2
+
3
+ __version__ = check_notte_version("notte_browser")
@@ -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