notte-browser 1.4.0__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.
Files changed (48) hide show
  1. notte_browser-1.4.0/.gitignore +179 -0
  2. notte_browser-1.4.0/PKG-INFO +16 -0
  3. notte_browser-1.4.0/README.md +5 -0
  4. notte_browser-1.4.0/pyproject.toml +28 -0
  5. notte_browser-1.4.0/src/notte_browser/__init__.py +3 -0
  6. notte_browser-1.4.0/src/notte_browser/controller.py +219 -0
  7. notte_browser-1.4.0/src/notte_browser/dom/__init__.py +0 -0
  8. notte_browser-1.4.0/src/notte_browser/dom/buildDomNode.js +516 -0
  9. notte_browser-1.4.0/src/notte_browser/dom/csspaths.py +155 -0
  10. notte_browser-1.4.0/src/notte_browser/dom/dropdown_menu.py +162 -0
  11. notte_browser-1.4.0/src/notte_browser/dom/id_generation.py +39 -0
  12. notte_browser-1.4.0/src/notte_browser/dom/locate.py +80 -0
  13. notte_browser-1.4.0/src/notte_browser/dom/parsing.py +158 -0
  14. notte_browser-1.4.0/src/notte_browser/dom/pipe.py +13 -0
  15. notte_browser-1.4.0/src/notte_browser/dom/types.py +465 -0
  16. notte_browser-1.4.0/src/notte_browser/dom/wait_for_page_update.py +132 -0
  17. notte_browser-1.4.0/src/notte_browser/env.py +454 -0
  18. notte_browser-1.4.0/src/notte_browser/errors.py +278 -0
  19. notte_browser-1.4.0/src/notte_browser/py.typed +0 -0
  20. notte_browser-1.4.0/src/notte_browser/rendering/__init__.py +0 -0
  21. notte_browser-1.4.0/src/notte_browser/rendering/interaction_only.py +125 -0
  22. notte_browser-1.4.0/src/notte_browser/rendering/json.py +47 -0
  23. notte_browser-1.4.0/src/notte_browser/rendering/markdown.py +71 -0
  24. notte_browser-1.4.0/src/notte_browser/rendering/pipe.py +87 -0
  25. notte_browser-1.4.0/src/notte_browser/rendering/pruning.py +124 -0
  26. notte_browser-1.4.0/src/notte_browser/resolution.py +118 -0
  27. notte_browser-1.4.0/src/notte_browser/resource.py +284 -0
  28. notte_browser-1.4.0/src/notte_browser/scraping/__init__.py +0 -0
  29. notte_browser-1.4.0/src/notte_browser/scraping/llm_scraping.py +60 -0
  30. notte_browser-1.4.0/src/notte_browser/scraping/pipe.py +142 -0
  31. notte_browser-1.4.0/src/notte_browser/scraping/schema.py +145 -0
  32. notte_browser-1.4.0/src/notte_browser/scraping/simple.py +21 -0
  33. notte_browser-1.4.0/src/notte_browser/tagging/__init__.py +0 -0
  34. notte_browser-1.4.0/src/notte_browser/tagging/action/__init__.py +0 -0
  35. notte_browser-1.4.0/src/notte_browser/tagging/action/base.py +26 -0
  36. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/__init__.py +0 -0
  37. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/base.py +130 -0
  38. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/filtering.py +34 -0
  39. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/listing.py +146 -0
  40. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/parser.py +348 -0
  41. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/pipe.py +216 -0
  42. notte_browser-1.4.0/src/notte_browser/tagging/action/llm_taging/validation.py +40 -0
  43. notte_browser-1.4.0/src/notte_browser/tagging/action/pipe.py +75 -0
  44. notte_browser-1.4.0/src/notte_browser/tagging/action/simple/__init__.py +0 -0
  45. notte_browser-1.4.0/src/notte_browser/tagging/action/simple/pipe.py +71 -0
  46. notte_browser-1.4.0/src/notte_browser/tagging/page.py +35 -0
  47. notte_browser-1.4.0/src/notte_browser/vault.py +28 -0
  48. notte_browser-1.4.0/src/notte_browser/window.py +364 -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: 1.4.0
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>=1.3.3
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,5 @@
1
+ # Notte Processing Pipelines
2
+
3
+ Each pipeline is a set of steps that are used to preprocess either a browser snapshot or a LLM observation.
4
+
5
+ Each file in this directory is a separate but mandatory in the global workflow.
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "notte-browser"
3
+ version = "1.4.0"
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>=1.3.3",
17
+ "patchright==1.50.0",
18
+ "maincontentextractor",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [dependency-groups]
26
+
27
+ [tool.uv.sources]
28
+ maincontentextractor = { git = "https://github.com/HawkClaws/main_content_extractor", rev = "7c3ed7f6ed7f6c10223a3357d43ab741663bc812" }
@@ -0,0 +1,3 @@
1
+ from notte_core import check_notte_version
2
+
3
+ __version__ = check_notte_version("notte_browser")
@@ -0,0 +1,219 @@
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, window: BrowserWindow, verbose: bool = False) -> None:
39
+ self.window: BrowserWindow = window
40
+ self.verbose: bool = verbose
41
+
42
+ self.execute = capture_playwright_errors(verbose=verbose)(self.execute) # type: ignore[reportAttributeAccessIssue]
43
+
44
+ async def switch_tab(self, tab_index: int) -> None:
45
+ context = self.window.page.context
46
+ if tab_index != -1 and (tab_index < 0 or tab_index >= len(context.pages)):
47
+ raise ValueError(f"Tab index '{tab_index}' is out of range for context with {len(context.pages)} pages")
48
+ tab_page = context.pages[tab_index]
49
+ await tab_page.bring_to_front()
50
+ self.window.page = tab_page
51
+ await self.window.long_wait()
52
+ if self.verbose:
53
+ logger.info(
54
+ f"🪦 Switched to tab {tab_index} with url: {tab_page.url} ({len(context.pages)} tabs in context)"
55
+ )
56
+
57
+ async def execute_browser_action(self, action: BaseAction) -> BrowserSnapshot | None:
58
+ match action:
59
+ case GotoAction(url=url):
60
+ return await self.window.goto(url)
61
+ case GotoNewTabAction(url=url):
62
+ new_page = await self.window.page.context.new_page()
63
+ self.window.page = new_page
64
+ _ = await new_page.goto(url)
65
+ case SwitchTabAction(tab_index=tab_index):
66
+ await self.switch_tab(tab_index)
67
+ case WaitAction(time_ms=time_ms):
68
+ await self.window.page.wait_for_timeout(time_ms)
69
+ case GoBackAction():
70
+ _ = await self.window.page.go_back()
71
+ case GoForwardAction():
72
+ _ = await self.window.page.go_forward()
73
+ case ReloadAction():
74
+ _ = await self.window.page.reload()
75
+ await self.window.long_wait()
76
+ case PressKeyAction(key=key):
77
+ await self.window.page.keyboard.press(key)
78
+ case ScrollUpAction(amount=amount):
79
+ if amount is not None:
80
+ await self.window.page.mouse.wheel(delta_x=0, delta_y=-amount)
81
+ else:
82
+ await self.window.page.keyboard.press("PageUp")
83
+ case ScrollDownAction(amount=amount):
84
+ if amount is not None:
85
+ await self.window.page.mouse.wheel(delta_x=0, delta_y=amount)
86
+ else:
87
+ await self.window.page.keyboard.press("PageDown")
88
+ case ScrapeAction():
89
+ raise NotImplementedError("Scrape action is not supported in the browser controller")
90
+ case _:
91
+ raise ValueError(f"Unsupported action type: {type(action)}")
92
+
93
+ # perform snapshot in execute
94
+ return None
95
+
96
+ async def execute_interaction_action(self, action: InteractionAction) -> BrowserSnapshot | None:
97
+ if action.selector is None:
98
+ raise ValueError(f"Selector is required for {action.name()}")
99
+ press_enter = False
100
+ if action.press_enter is not None:
101
+ press_enter = action.press_enter
102
+ # locate element (possibly in iframe)
103
+ locator: Locator = await locate_element(self.window.page, action.selector)
104
+ original_url = self.window.page.url
105
+
106
+ action_timeout = self.window.config.wait.action_timeout
107
+
108
+ match action:
109
+ # Interaction actions
110
+ case ClickAction():
111
+ await locator.click(timeout=action_timeout)
112
+ case FillAction(value=value):
113
+ if text_contains_tabs(text=get_str_value(value)):
114
+ if self.verbose:
115
+ logger.info(
116
+ "🪦 Indentation detected in fill action: simulating clipboard copy/paste for better string formatting"
117
+ )
118
+ await locator.focus()
119
+
120
+ if action.clear_before_fill:
121
+ await self.window.page.keyboard.press(key=f"{platform_control_key()}+A")
122
+ await self.window.short_wait()
123
+ await self.window.page.keyboard.press(key="Backspace")
124
+ await self.window.short_wait()
125
+
126
+ # Use isolated clipboard variable instead of system clipboard
127
+ await self.window.page.evaluate(
128
+ """
129
+ (text) => {
130
+ window.__isolatedClipboard = text;
131
+ const dataTransfer = new DataTransfer();
132
+ dataTransfer.setData('text/plain', window.__isolatedClipboard);
133
+ document.activeElement.dispatchEvent(new ClipboardEvent('paste', {
134
+ clipboardData: dataTransfer,
135
+ bubbles: true,
136
+ cancelable: true
137
+ }));
138
+ }
139
+ """,
140
+ value,
141
+ )
142
+
143
+ await self.window.short_wait()
144
+ else:
145
+ await locator.fill(get_str_value(value), timeout=action_timeout, force=action.clear_before_fill)
146
+ await self.window.short_wait()
147
+ case CheckAction(value=value):
148
+ if value:
149
+ await locator.check()
150
+ else:
151
+ await locator.uncheck()
152
+ case SelectDropdownOptionAction(value=value, option_selector=option_selector):
153
+ # Check if it's a standard HTML select
154
+ tag_name: str = await locator.evaluate("el => el.tagName.toLowerCase()")
155
+ if tag_name == "select":
156
+ # Handle standard HTML select
157
+ _ = await locator.select_option(value)
158
+ elif option_selector is None:
159
+ raise ValueError(f"Option selector is required for {action.name()}")
160
+ else:
161
+ option_locator = await locate_element(self.window.page, option_selector)
162
+ # Handle non-standard select
163
+ await option_locator.click()
164
+
165
+ case ListDropdownOptionsAction():
166
+ options = await dropdown_menu_options(self.window.page, action.selector.xpath_selector)
167
+ if self.verbose:
168
+ logger.info(f"Dropdown options: {options}")
169
+ raise NotImplementedError("ListDropdownOptionsAction is not supported in the browser controller")
170
+ case _:
171
+ raise ValueError(f"Unsupported action type: {type(action)}")
172
+ if press_enter:
173
+ if self.verbose:
174
+ logger.info(f"🪦 Pressing enter for action {action.id}")
175
+ await self.window.short_wait()
176
+ await self.window.page.keyboard.press("Enter")
177
+ if original_url != self.window.page.url:
178
+ if self.verbose:
179
+ logger.info(f"🪦 Page navigation detected for action {action.id} waiting for networkidle")
180
+ await self.window.long_wait()
181
+
182
+ # perform snapshot in execute
183
+ return None
184
+
185
+ async def execute(self, action: BaseAction) -> BrowserSnapshot:
186
+ context = self.window.page.context
187
+ num_pages = len(context.pages)
188
+ match action:
189
+ case InteractionAction():
190
+ retval = await self.execute_interaction_action(action)
191
+ case CompletionAction(success=success, answer=answer):
192
+ snapshot = await self.window.snapshot()
193
+ if self.verbose:
194
+ logger.info(
195
+ f"Completion action: status={'success' if success else 'failure'} with answer = {answer}"
196
+ )
197
+ await self.window.close()
198
+ return snapshot
199
+ case _:
200
+ retval = await self.execute_browser_action(action)
201
+ # add short wait before we check for new tabs to make sure that
202
+ # the page has time to be created
203
+ await self.window.short_wait()
204
+ if len(context.pages) != num_pages:
205
+ if self.verbose:
206
+ logger.info(f"🪦 Action {action.id} resulted in a new tab, switched to it...")
207
+ await self.switch_tab(tab_index=-1)
208
+ elif retval is not None:
209
+ # only return snapshot if we didn't switch to a new tab
210
+ # otherwise, the snapshot is out of date and we need to take a new one
211
+ return retval
212
+
213
+ return await self.window.snapshot()
214
+
215
+ async def execute_multiple(self, actions: list[BaseAction]) -> list[BrowserSnapshot]:
216
+ snapshots: list[BrowserSnapshot] = []
217
+ for action in actions:
218
+ snapshots.append(await self.execute(action))
219
+ return snapshots
File without changes