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