openhands-tools 1.7.0__tar.gz → 1.7.2__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.
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/PKG-INFO +1 -1
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/browser_use/__init__.py +8 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/browser_use/definition.py +128 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/browser_use/impl.py +16 -0
- openhands_tools-1.7.2/openhands/tools/browser_use/server.py +190 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/tmux_terminal.py +5 -3
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/PKG-INFO +1 -1
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/pyproject.toml +1 -1
- openhands_tools-1.7.0/openhands/tools/browser_use/server.py +0 -100
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/apply_patch/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/apply_patch/core.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/apply_patch/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/browser_use/impl_windows.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/browser_use/logging_fix.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/registration.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/templates/delegate_tool_description.j2 +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/delegate/visualizer.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/editor.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/exceptions.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/config.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/constants.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/diff.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/encoding.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/file_cache.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/history.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/shell.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/glob/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/glob/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/glob/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/grep/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/grep/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/grep/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/preset/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/preset/default.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/preset/gemini.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/preset/planning.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/py.typed +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/task_tracker/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/task_tracker/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/constants.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/impl.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/metadata.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/factory.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/interface.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/utils/command.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/tom_consult/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/tom_consult/definition.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/tom_consult/executor.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/utils/__init__.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/utils/timeout.py +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/SOURCES.txt +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/dependency_links.txt +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/requires.txt +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/top_level.txt +0 -0
- {openhands_tools-1.7.0 → openhands_tools-1.7.2}/setup.cfg +0 -0
|
@@ -9,6 +9,8 @@ from openhands.tools.browser_use.definition import (
|
|
|
9
9
|
BrowserGetContentTool,
|
|
10
10
|
BrowserGetStateAction,
|
|
11
11
|
BrowserGetStateTool,
|
|
12
|
+
BrowserGetStorageAction,
|
|
13
|
+
BrowserGetStorageTool,
|
|
12
14
|
BrowserGoBackAction,
|
|
13
15
|
BrowserGoBackTool,
|
|
14
16
|
BrowserListTabsAction,
|
|
@@ -18,6 +20,8 @@ from openhands.tools.browser_use.definition import (
|
|
|
18
20
|
BrowserObservation,
|
|
19
21
|
BrowserScrollAction,
|
|
20
22
|
BrowserScrollTool,
|
|
23
|
+
BrowserSetStorageAction,
|
|
24
|
+
BrowserSetStorageTool,
|
|
21
25
|
BrowserSwitchTabAction,
|
|
22
26
|
BrowserSwitchTabTool,
|
|
23
27
|
BrowserToolSet,
|
|
@@ -38,6 +42,8 @@ __all__ = [
|
|
|
38
42
|
"BrowserListTabsTool",
|
|
39
43
|
"BrowserSwitchTabTool",
|
|
40
44
|
"BrowserCloseTabTool",
|
|
45
|
+
"BrowserGetStorageTool",
|
|
46
|
+
"BrowserSetStorageTool",
|
|
41
47
|
# Actions
|
|
42
48
|
"BrowserNavigateAction",
|
|
43
49
|
"BrowserClickAction",
|
|
@@ -49,6 +55,8 @@ __all__ = [
|
|
|
49
55
|
"BrowserListTabsAction",
|
|
50
56
|
"BrowserSwitchTabAction",
|
|
51
57
|
"BrowserCloseTabAction",
|
|
58
|
+
"BrowserGetStorageAction",
|
|
59
|
+
"BrowserSetStorageAction",
|
|
52
60
|
# Observations
|
|
53
61
|
"BrowserObservation",
|
|
54
62
|
"BrowserToolSet",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Browser-use tool implementation for web automation."""
|
|
2
2
|
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
3
5
|
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
from typing import TYPE_CHECKING, Literal, Self
|
|
5
8
|
|
|
6
9
|
from pydantic import Field
|
|
@@ -57,6 +60,29 @@ class BrowserObservation(Observation):
|
|
|
57
60
|
description="Directory where full output files are saved",
|
|
58
61
|
)
|
|
59
62
|
|
|
63
|
+
def _save_screenshot(self, base64_data: str, save_dir: str) -> str | None:
|
|
64
|
+
try:
|
|
65
|
+
save_dir_path = Path(save_dir)
|
|
66
|
+
save_dir_path.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
mime_type = detect_image_mime_type(base64_data)
|
|
69
|
+
ext = mime_type.split("/")[-1]
|
|
70
|
+
if ext == "jpeg":
|
|
71
|
+
ext = "jpg"
|
|
72
|
+
|
|
73
|
+
# Generate hash for filename
|
|
74
|
+
content_hash = hashlib.sha256(base64_data.encode("utf-8")).hexdigest()[:8]
|
|
75
|
+
filename = f"browser_screenshot_{content_hash}.{ext}"
|
|
76
|
+
file_path = save_dir_path / filename
|
|
77
|
+
|
|
78
|
+
if not file_path.exists():
|
|
79
|
+
image_data = base64.b64decode(base64_data)
|
|
80
|
+
file_path.write_bytes(image_data)
|
|
81
|
+
|
|
82
|
+
return str(file_path)
|
|
83
|
+
except Exception:
|
|
84
|
+
return None
|
|
85
|
+
|
|
60
86
|
@property
|
|
61
87
|
def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
|
|
62
88
|
llm_content: list[TextContent | ImageContent] = []
|
|
@@ -81,6 +107,17 @@ class BrowserObservation(Observation):
|
|
|
81
107
|
|
|
82
108
|
if self.screenshot_data:
|
|
83
109
|
mime_type = detect_image_mime_type(self.screenshot_data)
|
|
110
|
+
|
|
111
|
+
# Save screenshot if directory is available
|
|
112
|
+
if self.full_output_save_dir:
|
|
113
|
+
saved_path = self._save_screenshot(
|
|
114
|
+
self.screenshot_data, self.full_output_save_dir
|
|
115
|
+
)
|
|
116
|
+
if saved_path:
|
|
117
|
+
llm_content.append(
|
|
118
|
+
TextContent(text=f"Screenshot saved to: {saved_path}")
|
|
119
|
+
)
|
|
120
|
+
|
|
84
121
|
# Convert base64 to data URL format for ImageContent
|
|
85
122
|
data_url = f"data:{mime_type};base64,{self.screenshot_data}"
|
|
86
123
|
llm_content.append(ImageContent(image_urls=[data_url]))
|
|
@@ -542,6 +579,95 @@ class BrowserCloseTabTool(ToolDefinition[BrowserCloseTabAction, BrowserObservati
|
|
|
542
579
|
]
|
|
543
580
|
|
|
544
581
|
|
|
582
|
+
# ============================================
|
|
583
|
+
# `browser_get_storage`
|
|
584
|
+
# ============================================
|
|
585
|
+
class BrowserGetStorageAction(BrowserAction):
|
|
586
|
+
"""Schema for getting browser storage (cookies, local storage, session storage)."""
|
|
587
|
+
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
BROWSER_GET_STORAGE_DESCRIPTION = """Get browser storage data including cookies,
|
|
592
|
+
local storage, and session storage.
|
|
593
|
+
|
|
594
|
+
This tool extracts all cookies and storage data from the current browser session.
|
|
595
|
+
Useful for debugging, session management, or extracting authentication tokens.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class BrowserGetStorageTool(
|
|
600
|
+
ToolDefinition[BrowserGetStorageAction, BrowserObservation]
|
|
601
|
+
):
|
|
602
|
+
"""Tool for getting browser storage."""
|
|
603
|
+
|
|
604
|
+
@classmethod
|
|
605
|
+
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
|
|
606
|
+
return [
|
|
607
|
+
cls(
|
|
608
|
+
description=BROWSER_GET_STORAGE_DESCRIPTION,
|
|
609
|
+
action_type=BrowserGetStorageAction,
|
|
610
|
+
observation_type=BrowserObservation,
|
|
611
|
+
annotations=ToolAnnotations(
|
|
612
|
+
title="browser_get_storage",
|
|
613
|
+
readOnlyHint=True,
|
|
614
|
+
destructiveHint=False,
|
|
615
|
+
idempotentHint=True,
|
|
616
|
+
openWorldHint=False,
|
|
617
|
+
),
|
|
618
|
+
executor=executor,
|
|
619
|
+
)
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ============================================
|
|
624
|
+
# `browser_set_storage`
|
|
625
|
+
# ============================================
|
|
626
|
+
class BrowserSetStorageAction(BrowserAction):
|
|
627
|
+
"""Schema for setting browser storage (cookies, local storage, session storage)."""
|
|
628
|
+
|
|
629
|
+
storage_state: dict = Field(
|
|
630
|
+
description="Storage state dictionary containing 'cookies' and 'origins' (from browser_get_storage)" # noqa: E501
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
BROWSER_SET_STORAGE_DESCRIPTION = """Set browser storage data including cookies,
|
|
635
|
+
local storage, and session storage.
|
|
636
|
+
|
|
637
|
+
This tool allows you to restore or set the browser's storage state. You can use the
|
|
638
|
+
output from browser_get_storage to restore a previous session.
|
|
639
|
+
|
|
640
|
+
Parameters:
|
|
641
|
+
- storage_state: A dictionary containing 'cookies' and 'origins'.
|
|
642
|
+
- cookies: List of cookie objects
|
|
643
|
+
- origins: List of origin objects containing 'localStorage' and 'sessionStorage'
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class BrowserSetStorageTool(
|
|
648
|
+
ToolDefinition[BrowserSetStorageAction, BrowserObservation]
|
|
649
|
+
):
|
|
650
|
+
"""Tool for setting browser storage."""
|
|
651
|
+
|
|
652
|
+
@classmethod
|
|
653
|
+
def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
|
|
654
|
+
return [
|
|
655
|
+
cls(
|
|
656
|
+
description=BROWSER_SET_STORAGE_DESCRIPTION,
|
|
657
|
+
action_type=BrowserSetStorageAction,
|
|
658
|
+
observation_type=BrowserObservation,
|
|
659
|
+
annotations=ToolAnnotations(
|
|
660
|
+
title="browser_set_storage",
|
|
661
|
+
readOnlyHint=False,
|
|
662
|
+
destructiveHint=True,
|
|
663
|
+
idempotentHint=False,
|
|
664
|
+
openWorldHint=False,
|
|
665
|
+
),
|
|
666
|
+
executor=executor,
|
|
667
|
+
)
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
|
|
545
671
|
class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
|
|
546
672
|
"""A set of all browser tools.
|
|
547
673
|
|
|
@@ -593,6 +719,8 @@ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
|
|
|
593
719
|
BrowserListTabsTool,
|
|
594
720
|
BrowserSwitchTabTool,
|
|
595
721
|
BrowserCloseTabTool,
|
|
722
|
+
BrowserGetStorageTool,
|
|
723
|
+
BrowserSetStorageTool,
|
|
596
724
|
]:
|
|
597
725
|
tools.extend(tool_class.create(executor))
|
|
598
726
|
return tools
|
|
@@ -214,11 +214,13 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
|
|
|
214
214
|
BrowserCloseTabAction,
|
|
215
215
|
BrowserGetContentAction,
|
|
216
216
|
BrowserGetStateAction,
|
|
217
|
+
BrowserGetStorageAction,
|
|
217
218
|
BrowserGoBackAction,
|
|
218
219
|
BrowserListTabsAction,
|
|
219
220
|
BrowserNavigateAction,
|
|
220
221
|
BrowserObservation,
|
|
221
222
|
BrowserScrollAction,
|
|
223
|
+
BrowserSetStorageAction,
|
|
222
224
|
BrowserSwitchTabAction,
|
|
223
225
|
BrowserTypeAction,
|
|
224
226
|
)
|
|
@@ -234,6 +236,10 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
|
|
|
234
236
|
result = await self.type_text(action.index, action.text)
|
|
235
237
|
elif isinstance(action, BrowserGetStateAction):
|
|
236
238
|
return await self.get_state(action.include_screenshot)
|
|
239
|
+
elif isinstance(action, BrowserGetStorageAction):
|
|
240
|
+
result = await self.get_storage()
|
|
241
|
+
elif isinstance(action, BrowserSetStorageAction):
|
|
242
|
+
result = await self.set_storage(action.storage_state)
|
|
237
243
|
elif isinstance(action, BrowserGetContentAction):
|
|
238
244
|
result = await self.get_content(
|
|
239
245
|
action.extract_links, action.start_from_char
|
|
@@ -334,6 +340,16 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
|
|
|
334
340
|
full_output_save_dir=self.full_output_save_dir,
|
|
335
341
|
)
|
|
336
342
|
|
|
343
|
+
async def get_storage(self) -> str:
|
|
344
|
+
"""Get browser storage (cookies, local storage, session storage)."""
|
|
345
|
+
await self._ensure_initialized()
|
|
346
|
+
return await self._server._get_storage()
|
|
347
|
+
|
|
348
|
+
async def set_storage(self, storage_state: dict) -> str:
|
|
349
|
+
"""Set browser storage (cookies, local storage, session storage)."""
|
|
350
|
+
await self._ensure_initialized()
|
|
351
|
+
return await self._server._set_storage(storage_state)
|
|
352
|
+
|
|
337
353
|
# Tab Management
|
|
338
354
|
async def list_tabs(self) -> str:
|
|
339
355
|
"""List all open tabs."""
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from browser_use.dom.markdown_extractor import extract_clean_markdown
|
|
2
|
+
|
|
3
|
+
from openhands.sdk import get_logger
|
|
4
|
+
from openhands.tools.browser_use.logging_fix import LogSafeBrowserUseServer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CustomBrowserUseServer(LogSafeBrowserUseServer):
|
|
11
|
+
"""
|
|
12
|
+
Custom BrowserUseServer with a new tool for extracting web
|
|
13
|
+
page's content in markdown.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
async def _get_storage(self) -> str:
|
|
17
|
+
"""Get browser storage (cookies, local storage, session storage)."""
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
if not self.browser_session:
|
|
21
|
+
return "Error: No browser session active"
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# Use the private method from BrowserSession to get storage state
|
|
25
|
+
# This returns a dict with 'cookies' and 'origins'
|
|
26
|
+
# (localStorage/sessionStorage)
|
|
27
|
+
storage_state = await self.browser_session._cdp_get_storage_state()
|
|
28
|
+
return json.dumps(storage_state, indent=2)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.exception("Error getting storage state", exc_info=e)
|
|
31
|
+
return f"Error getting storage state: {str(e)}"
|
|
32
|
+
|
|
33
|
+
async def _set_storage(self, storage_state: dict) -> str:
|
|
34
|
+
"""Set browser storage (cookies, local storage, session storage)."""
|
|
35
|
+
if not self.browser_session:
|
|
36
|
+
return "Error: No browser session active"
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# 1. Set cookies
|
|
40
|
+
cookies = storage_state.get("cookies", [])
|
|
41
|
+
if cookies:
|
|
42
|
+
await self.browser_session._cdp_set_cookies(cookies)
|
|
43
|
+
|
|
44
|
+
# 2. Set local/session storage
|
|
45
|
+
origins = storage_state.get("origins", [])
|
|
46
|
+
if origins:
|
|
47
|
+
cdp_session = await self.browser_session.get_or_create_cdp_session()
|
|
48
|
+
|
|
49
|
+
# Enable DOMStorage
|
|
50
|
+
await cdp_session.cdp_client.send.DOMStorage.enable(
|
|
51
|
+
session_id=cdp_session.session_id
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
for origin_data in origins:
|
|
56
|
+
origin = origin_data.get("origin")
|
|
57
|
+
if not origin:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
dom_storage = cdp_session.cdp_client.send.DOMStorage
|
|
61
|
+
|
|
62
|
+
# Set localStorage
|
|
63
|
+
for item in origin_data.get("localStorage", []):
|
|
64
|
+
key = item.get("key") or item.get("name")
|
|
65
|
+
if not key:
|
|
66
|
+
continue
|
|
67
|
+
await dom_storage.setDOMStorageItem(
|
|
68
|
+
params={
|
|
69
|
+
"storageId": {
|
|
70
|
+
"securityOrigin": origin,
|
|
71
|
+
"isLocalStorage": True,
|
|
72
|
+
},
|
|
73
|
+
"key": key,
|
|
74
|
+
"value": item["value"],
|
|
75
|
+
},
|
|
76
|
+
session_id=cdp_session.session_id,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Set sessionStorage
|
|
80
|
+
for item in origin_data.get("sessionStorage", []):
|
|
81
|
+
key = item.get("key") or item.get("name")
|
|
82
|
+
if not key:
|
|
83
|
+
continue
|
|
84
|
+
await dom_storage.setDOMStorageItem(
|
|
85
|
+
params={
|
|
86
|
+
"storageId": {
|
|
87
|
+
"securityOrigin": origin,
|
|
88
|
+
"isLocalStorage": False,
|
|
89
|
+
},
|
|
90
|
+
"key": key,
|
|
91
|
+
"value": item["value"],
|
|
92
|
+
},
|
|
93
|
+
session_id=cdp_session.session_id,
|
|
94
|
+
)
|
|
95
|
+
finally:
|
|
96
|
+
# Disable DOMStorage
|
|
97
|
+
await cdp_session.cdp_client.send.DOMStorage.disable(
|
|
98
|
+
session_id=cdp_session.session_id
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return "Storage set successfully"
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.exception("Error setting storage state", exc_info=e)
|
|
104
|
+
return f"Error setting storage state: {str(e)}"
|
|
105
|
+
|
|
106
|
+
async def _get_content(self, extract_links=False, start_from_char: int = 0) -> str:
|
|
107
|
+
MAX_CHAR_LIMIT = 30000
|
|
108
|
+
|
|
109
|
+
if not self.browser_session:
|
|
110
|
+
return "Error: No browser session active"
|
|
111
|
+
|
|
112
|
+
# Extract clean markdown using the new method
|
|
113
|
+
try:
|
|
114
|
+
content, content_stats = await extract_clean_markdown(
|
|
115
|
+
browser_session=self.browser_session, extract_links=extract_links
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.exception(
|
|
119
|
+
"Error extracting clean markdown", exc_info=e, stack_info=True
|
|
120
|
+
)
|
|
121
|
+
return f"Could not extract clean markdown: {type(e).__name__}"
|
|
122
|
+
|
|
123
|
+
# Original content length for processing
|
|
124
|
+
final_filtered_length = content_stats["final_filtered_chars"]
|
|
125
|
+
|
|
126
|
+
if start_from_char > 0:
|
|
127
|
+
if start_from_char >= len(content):
|
|
128
|
+
return f"start_from_char ({start_from_char}) exceeds content length ({len(content)}). Content has {final_filtered_length} characters after filtering." # noqa: E501
|
|
129
|
+
|
|
130
|
+
content = content[start_from_char:]
|
|
131
|
+
content_stats["started_from_char"] = start_from_char
|
|
132
|
+
|
|
133
|
+
# Smart truncation with context preservation
|
|
134
|
+
truncated = False
|
|
135
|
+
if len(content) > MAX_CHAR_LIMIT:
|
|
136
|
+
# Try to truncate at a natural break point (paragraph, sentence)
|
|
137
|
+
truncate_at = MAX_CHAR_LIMIT
|
|
138
|
+
|
|
139
|
+
# Look for paragraph break within last 500 chars of limit
|
|
140
|
+
paragraph_break = content.rfind(
|
|
141
|
+
"\n\n", MAX_CHAR_LIMIT - 500, MAX_CHAR_LIMIT
|
|
142
|
+
)
|
|
143
|
+
if paragraph_break > 0:
|
|
144
|
+
truncate_at = paragraph_break
|
|
145
|
+
else:
|
|
146
|
+
# Look for sentence break within last 200 chars of limit
|
|
147
|
+
sentence_break = content.rfind(
|
|
148
|
+
".", MAX_CHAR_LIMIT - 200, MAX_CHAR_LIMIT
|
|
149
|
+
)
|
|
150
|
+
if sentence_break > 0:
|
|
151
|
+
truncate_at = sentence_break + 1
|
|
152
|
+
|
|
153
|
+
content = content[:truncate_at]
|
|
154
|
+
truncated = True
|
|
155
|
+
next_start = (start_from_char or 0) + truncate_at
|
|
156
|
+
content_stats["truncated_at_char"] = truncate_at
|
|
157
|
+
content_stats["next_start_char"] = next_start
|
|
158
|
+
|
|
159
|
+
# Add content statistics to the result
|
|
160
|
+
original_html_length = content_stats["original_html_chars"]
|
|
161
|
+
initial_markdown_length = content_stats["initial_markdown_chars"]
|
|
162
|
+
chars_filtered = content_stats["filtered_chars_removed"]
|
|
163
|
+
|
|
164
|
+
stats_summary = (
|
|
165
|
+
f"Content processed: {original_html_length:,}"
|
|
166
|
+
+ f" HTML chars → {initial_markdown_length:,}"
|
|
167
|
+
+ f" initial markdown → {final_filtered_length:,} filtered markdown"
|
|
168
|
+
)
|
|
169
|
+
if start_from_char > 0:
|
|
170
|
+
stats_summary += f" (started from char {start_from_char:,})"
|
|
171
|
+
if truncated:
|
|
172
|
+
stats_summary += f" → {len(content):,} final chars (truncated, use start_from_char={content_stats['next_start_char']} to continue)" # noqa: E501
|
|
173
|
+
elif chars_filtered > 0:
|
|
174
|
+
stats_summary += f" (filtered {chars_filtered:,} chars of noise)"
|
|
175
|
+
|
|
176
|
+
prompt = f"""<content_stats>
|
|
177
|
+
{stats_summary}
|
|
178
|
+
</content_stats>
|
|
179
|
+
|
|
180
|
+
<webpage_content>
|
|
181
|
+
{content}
|
|
182
|
+
</webpage_content>"""
|
|
183
|
+
current_url = await self.browser_session.get_current_page_url()
|
|
184
|
+
|
|
185
|
+
return f"""<url>
|
|
186
|
+
{current_url}
|
|
187
|
+
</url>
|
|
188
|
+
<content>
|
|
189
|
+
{prompt}
|
|
190
|
+
</content>"""
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/tmux_terminal.py
RENAMED
|
@@ -97,9 +97,11 @@ class TmuxTerminal(TerminalInterface):
|
|
|
97
97
|
try:
|
|
98
98
|
if hasattr(self, "session"):
|
|
99
99
|
self.session.kill()
|
|
100
|
-
except
|
|
101
|
-
#
|
|
102
|
-
|
|
100
|
+
except Exception as e:
|
|
101
|
+
# Session might already be dead/killed externally
|
|
102
|
+
# (e.g., "can't find session" error from tmux)
|
|
103
|
+
# Also handles ImportError during Python shutdown
|
|
104
|
+
logger.debug(f"Error closing tmux session (may already be dead): {e}")
|
|
103
105
|
self._closed: bool = True
|
|
104
106
|
|
|
105
107
|
def send_keys(self, text: str, enter: bool = True) -> None:
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from browser_use.dom.markdown_extractor import extract_clean_markdown
|
|
2
|
-
|
|
3
|
-
from openhands.sdk import get_logger
|
|
4
|
-
from openhands.tools.browser_use.logging_fix import LogSafeBrowserUseServer
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
logger = get_logger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CustomBrowserUseServer(LogSafeBrowserUseServer):
|
|
11
|
-
"""
|
|
12
|
-
Custom BrowserUseServer with a new tool for extracting web
|
|
13
|
-
page's content in markdown.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
async def _get_content(self, extract_links=False, start_from_char: int = 0) -> str:
|
|
17
|
-
MAX_CHAR_LIMIT = 30000
|
|
18
|
-
|
|
19
|
-
if not self.browser_session:
|
|
20
|
-
return "Error: No browser session active"
|
|
21
|
-
|
|
22
|
-
# Extract clean markdown using the new method
|
|
23
|
-
try:
|
|
24
|
-
content, content_stats = await extract_clean_markdown(
|
|
25
|
-
browser_session=self.browser_session, extract_links=extract_links
|
|
26
|
-
)
|
|
27
|
-
except Exception as e:
|
|
28
|
-
logger.exception(
|
|
29
|
-
"Error extracting clean markdown", exc_info=e, stack_info=True
|
|
30
|
-
)
|
|
31
|
-
return f"Could not extract clean markdown: {type(e).__name__}"
|
|
32
|
-
|
|
33
|
-
# Original content length for processing
|
|
34
|
-
final_filtered_length = content_stats["final_filtered_chars"]
|
|
35
|
-
|
|
36
|
-
if start_from_char > 0:
|
|
37
|
-
if start_from_char >= len(content):
|
|
38
|
-
return f"start_from_char ({start_from_char}) exceeds content length ({len(content)}). Content has {final_filtered_length} characters after filtering." # noqa: E501
|
|
39
|
-
|
|
40
|
-
content = content[start_from_char:]
|
|
41
|
-
content_stats["started_from_char"] = start_from_char
|
|
42
|
-
|
|
43
|
-
# Smart truncation with context preservation
|
|
44
|
-
truncated = False
|
|
45
|
-
if len(content) > MAX_CHAR_LIMIT:
|
|
46
|
-
# Try to truncate at a natural break point (paragraph, sentence)
|
|
47
|
-
truncate_at = MAX_CHAR_LIMIT
|
|
48
|
-
|
|
49
|
-
# Look for paragraph break within last 500 chars of limit
|
|
50
|
-
paragraph_break = content.rfind(
|
|
51
|
-
"\n\n", MAX_CHAR_LIMIT - 500, MAX_CHAR_LIMIT
|
|
52
|
-
)
|
|
53
|
-
if paragraph_break > 0:
|
|
54
|
-
truncate_at = paragraph_break
|
|
55
|
-
else:
|
|
56
|
-
# Look for sentence break within last 200 chars of limit
|
|
57
|
-
sentence_break = content.rfind(
|
|
58
|
-
".", MAX_CHAR_LIMIT - 200, MAX_CHAR_LIMIT
|
|
59
|
-
)
|
|
60
|
-
if sentence_break > 0:
|
|
61
|
-
truncate_at = sentence_break + 1
|
|
62
|
-
|
|
63
|
-
content = content[:truncate_at]
|
|
64
|
-
truncated = True
|
|
65
|
-
next_start = (start_from_char or 0) + truncate_at
|
|
66
|
-
content_stats["truncated_at_char"] = truncate_at
|
|
67
|
-
content_stats["next_start_char"] = next_start
|
|
68
|
-
|
|
69
|
-
# Add content statistics to the result
|
|
70
|
-
original_html_length = content_stats["original_html_chars"]
|
|
71
|
-
initial_markdown_length = content_stats["initial_markdown_chars"]
|
|
72
|
-
chars_filtered = content_stats["filtered_chars_removed"]
|
|
73
|
-
|
|
74
|
-
stats_summary = (
|
|
75
|
-
f"Content processed: {original_html_length:,}"
|
|
76
|
-
+ f" HTML chars → {initial_markdown_length:,}"
|
|
77
|
-
+ f" initial markdown → {final_filtered_length:,} filtered markdown"
|
|
78
|
-
)
|
|
79
|
-
if start_from_char > 0:
|
|
80
|
-
stats_summary += f" (started from char {start_from_char:,})"
|
|
81
|
-
if truncated:
|
|
82
|
-
stats_summary += f" → {len(content):,} final chars (truncated, use start_from_char={content_stats['next_start_char']} to continue)" # noqa: E501
|
|
83
|
-
elif chars_filtered > 0:
|
|
84
|
-
stats_summary += f" (filtered {chars_filtered:,} chars of noise)"
|
|
85
|
-
|
|
86
|
-
prompt = f"""<content_stats>
|
|
87
|
-
{stats_summary}
|
|
88
|
-
</content_stats>
|
|
89
|
-
|
|
90
|
-
<webpage_content>
|
|
91
|
-
{content}
|
|
92
|
-
</webpage_content>"""
|
|
93
|
-
current_url = await self.browser_session.get_current_page_url()
|
|
94
|
-
|
|
95
|
-
return f"""<url>
|
|
96
|
-
{current_url}
|
|
97
|
-
</url>
|
|
98
|
-
<content>
|
|
99
|
-
{prompt}
|
|
100
|
-
</content>"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/encoding.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/file_cache.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/history.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/impl.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/factory.py
RENAMED
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.7.0 → openhands_tools-1.7.2}/openhands_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|