meshagent-computers 0.45.4__tar.gz → 0.45.6__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.
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/CHANGELOG.md +9 -0
- {meshagent_computers-0.45.4/meshagent_computers.egg-info → meshagent_computers-0.45.6}/PKG-INFO +4 -4
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/agent.py +2 -2
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/agent_test.py +379 -1
- meshagent_computers-0.45.6/meshagent/computers/browserbase_test.py +133 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/container_playwright_test.py +2 -2
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/docker.py +2 -2
- meshagent_computers-0.45.6/meshagent/computers/docker_test.py +167 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/stagehand_test.py +141 -2
- meshagent_computers-0.45.6/meshagent/computers/utils_test.py +102 -0
- meshagent_computers-0.45.6/meshagent/computers/version.py +1 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6/meshagent_computers.egg-info}/PKG-INFO +4 -4
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/SOURCES.txt +1 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/requires.txt +3 -3
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/pyproject.toml +3 -3
- meshagent_computers-0.45.4/meshagent/computers/docker_test.py +0 -22
- meshagent_computers-0.45.4/meshagent/computers/utils_test.py +0 -47
- meshagent_computers-0.45.4/meshagent/computers/version.py +0 -1
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/LICENSE +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/MANIFEST.in +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/README.md +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/__init__.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/base_playwright.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/browserbase.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/computer.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/container_playwright.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/local_playwright.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/operator.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/operator_test.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/stagehand.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/utils.py +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/dependency_links.txt +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/top_level.txt +0 -0
- {meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/setup.cfg +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [0.45.6]
|
|
2
|
+
- Added strict dataset prompt rendering with escaped-brace support and simple field placeholders, replacing generic `str.format` expansion.
|
|
3
|
+
- Tightened OpenAI adapter handling so code-interpreter results must be structured objects with required string fields, and invalid payloads now error out earlier.
|
|
4
|
+
- Aligned computer/browser tooling with Python behavior for Browserbase setup, Docker geometry handling, and optional rule initialization.
|
|
5
|
+
- Synchronized Python package dependencies and release metadata across the API, agents, CLI, OpenAI, tools, computers, codex, and related companion packages.
|
|
6
|
+
|
|
7
|
+
## [0.45.5]
|
|
8
|
+
- Shell command analysis now handles mixed absolute and dynamic write targets more safely, keeping generated file previews and path grouping accurate.
|
|
9
|
+
|
|
1
10
|
## [0.45.4]
|
|
2
11
|
- Stability
|
|
3
12
|
|
{meshagent_computers-0.45.4/meshagent_computers.egg-info → meshagent_computers-0.45.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-computers
|
|
3
|
-
Version: 0.45.
|
|
3
|
+
Version: 0.45.6
|
|
4
4
|
Summary: Computer Building Blocks for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -12,9 +12,9 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: pytest~=8.4
|
|
13
13
|
Requires-Dist: pytest-asyncio~=0.26
|
|
14
14
|
Requires-Dist: openai~=2.25.0
|
|
15
|
-
Requires-Dist: meshagent-api==0.45.
|
|
16
|
-
Requires-Dist: meshagent-agents==0.45.
|
|
17
|
-
Requires-Dist: meshagent-tools==0.45.
|
|
15
|
+
Requires-Dist: meshagent-api==0.45.6
|
|
16
|
+
Requires-Dist: meshagent-agents==0.45.6
|
|
17
|
+
Requires-Dist: meshagent-tools==0.45.6
|
|
18
18
|
Requires-Dist: playwright~=1.58.0
|
|
19
19
|
Requires-Dist: stagehand~=3.6.0
|
|
20
20
|
Requires-Dist: browserbase~=1.2
|
|
@@ -45,7 +45,7 @@ class ComputerTool(OpenAIResponsesTool):
|
|
|
45
45
|
computer: Computer,
|
|
46
46
|
title="computer_call",
|
|
47
47
|
description="handle computer tool calls",
|
|
48
|
-
rules
|
|
48
|
+
rules: Optional[list[str]] = None,
|
|
49
49
|
render_screen: Optional[Callable[[bytes], Awaitable[None] | None]] = None,
|
|
50
50
|
toolkit: "ComputerToolkit",
|
|
51
51
|
):
|
|
@@ -54,7 +54,7 @@ class ComputerTool(OpenAIResponsesTool):
|
|
|
54
54
|
# TODO: give a correct schema
|
|
55
55
|
title=title,
|
|
56
56
|
description=description,
|
|
57
|
-
rules=rules,
|
|
57
|
+
rules=[] if rules is None else rules,
|
|
58
58
|
)
|
|
59
59
|
self.operator = operator
|
|
60
60
|
self.computer = computer
|
|
@@ -7,7 +7,7 @@ from meshagent.computers import agent as agent_module
|
|
|
7
7
|
from meshagent.computers import base_playwright as base_playwright_module
|
|
8
8
|
from meshagent.computers.agent import ComputerToolkit
|
|
9
9
|
from meshagent.computers.base_playwright import BasePlaywrightComputer
|
|
10
|
-
from meshagent.computers.computer import ComputerContext
|
|
10
|
+
from meshagent.computers.computer import Computer, ComputerContext
|
|
11
11
|
from meshagent.tools import ToolContext
|
|
12
12
|
|
|
13
13
|
|
|
@@ -43,6 +43,20 @@ class _FakeRoom:
|
|
|
43
43
|
self.local_participant = _FakeParticipant(name=name)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class _FakeStorage:
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.uploads: list[dict[str, Any]] = []
|
|
49
|
+
|
|
50
|
+
async def upload(self, **kwargs: Any) -> None:
|
|
51
|
+
self.uploads.append(kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _FakeRoomWithStorage(_FakeRoom):
|
|
55
|
+
def __init__(self, name: str):
|
|
56
|
+
super().__init__(name=name)
|
|
57
|
+
self.storage = _FakeStorage()
|
|
58
|
+
|
|
59
|
+
|
|
46
60
|
class _FakeOperator:
|
|
47
61
|
def __init__(self):
|
|
48
62
|
self.calls: list[dict[str, Any]] = []
|
|
@@ -67,6 +81,204 @@ def test_computer_toolkit_has_no_default_render_screen():
|
|
|
67
81
|
assert toolkit.render_screen is None
|
|
68
82
|
|
|
69
83
|
|
|
84
|
+
def test_computer_tool_does_not_share_default_rules_list():
|
|
85
|
+
first = ComputerToolkit(
|
|
86
|
+
computer=_FakeComputer(),
|
|
87
|
+
operator=_FakeOperator(),
|
|
88
|
+
room=_FakeRoom(name="agent"),
|
|
89
|
+
)
|
|
90
|
+
second = ComputerToolkit(
|
|
91
|
+
computer=_FakeComputer(),
|
|
92
|
+
operator=_FakeOperator(),
|
|
93
|
+
room=_FakeRoom(name="agent"),
|
|
94
|
+
)
|
|
95
|
+
first_tool = next(tool for tool in first.tools if tool.name == "computer_call")
|
|
96
|
+
second_tool = next(tool for tool in second.tools if tool.name == "computer_call")
|
|
97
|
+
|
|
98
|
+
first_tool.rules.append("first only")
|
|
99
|
+
|
|
100
|
+
assert first_tool.rules == ["first only"]
|
|
101
|
+
assert second_tool.rules == []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_computer_tool_preserves_explicit_rules_list_reference():
|
|
105
|
+
rules: list[str] = []
|
|
106
|
+
toolkit = ComputerToolkit(
|
|
107
|
+
computer=_FakeComputer(),
|
|
108
|
+
operator=_FakeOperator(),
|
|
109
|
+
room=_FakeRoom(name="agent"),
|
|
110
|
+
)
|
|
111
|
+
tool = type(next(tool for tool in toolkit.tools if tool.name == "computer_call"))(
|
|
112
|
+
computer=_FakeComputer(),
|
|
113
|
+
operator=_FakeOperator(),
|
|
114
|
+
toolkit=toolkit,
|
|
115
|
+
rules=rules,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
rules.append("later")
|
|
119
|
+
|
|
120
|
+
assert tool.rules is rules
|
|
121
|
+
assert tool.rules == ["later"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_computer_context_emit_invokes_event_handler_like_python():
|
|
125
|
+
events: list[dict[str, Any]] = []
|
|
126
|
+
room = _FakeRoom(name="agent")
|
|
127
|
+
context = ComputerContext(
|
|
128
|
+
room=room,
|
|
129
|
+
caller=room.local_participant,
|
|
130
|
+
event_handler=events.append,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
event = {"type": "custom", "nested": {"value": 1}}
|
|
134
|
+
context.emit(event)
|
|
135
|
+
ComputerContext(room=room, caller=room.local_participant).emit({"ignored": True})
|
|
136
|
+
|
|
137
|
+
assert events == [event]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_computer_context_startup_events_match_python_dedupe_and_details():
|
|
141
|
+
events: list[dict[str, Any]] = []
|
|
142
|
+
room = _FakeRoom(name="agent")
|
|
143
|
+
|
|
144
|
+
def make_startup_event(state: str, details: tuple[str, ...]) -> dict[str, Any]:
|
|
145
|
+
return {"type": "startup", "state": state, "details": list(details)}
|
|
146
|
+
|
|
147
|
+
context = ComputerContext(
|
|
148
|
+
room=room,
|
|
149
|
+
caller=room.local_participant,
|
|
150
|
+
event_handler=events.append,
|
|
151
|
+
startup_event_factory=make_startup_event,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert context.room is room
|
|
155
|
+
assert context.caller is room.local_participant
|
|
156
|
+
assert context.on_behalf_of is None
|
|
157
|
+
assert context.last_startup_state is None
|
|
158
|
+
assert context.last_startup_details == ()
|
|
159
|
+
|
|
160
|
+
context.emit_startup(state="queued", details=[" one ", "", 7, "two"]) # type: ignore[list-item]
|
|
161
|
+
context.emit_startup(state="queued", details=("one", "two"))
|
|
162
|
+
context.emit_startup(state="in_progress")
|
|
163
|
+
|
|
164
|
+
assert events == [
|
|
165
|
+
{"type": "startup", "state": "queued", "details": ["one", "two"]},
|
|
166
|
+
{"type": "startup", "state": "in_progress", "details": []},
|
|
167
|
+
]
|
|
168
|
+
assert context.last_startup_state == "in_progress"
|
|
169
|
+
assert context.last_startup_details == ()
|
|
170
|
+
|
|
171
|
+
no_factory = ComputerContext(room=room, caller=room.local_participant)
|
|
172
|
+
no_factory.emit_startup(state="completed", details=["ignored"])
|
|
173
|
+
assert no_factory.last_startup_state is None
|
|
174
|
+
assert no_factory.last_startup_details == ()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_computer_protocol_default_context_manager_returns_self() -> None:
|
|
179
|
+
class _DefaultContextManagerComputer(Computer):
|
|
180
|
+
environment = "browser"
|
|
181
|
+
dimensions = (1, 1)
|
|
182
|
+
|
|
183
|
+
computer = _DefaultContextManagerComputer()
|
|
184
|
+
|
|
185
|
+
assert await computer.__aenter__(object()) is computer # type: ignore[arg-type]
|
|
186
|
+
assert (
|
|
187
|
+
await computer.__aexit__(Exception, Exception("boom"), object()) # type: ignore[arg-type]
|
|
188
|
+
is computer
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_base_playwright_constructor_helpers_match_python_branches(
|
|
193
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
194
|
+
) -> None:
|
|
195
|
+
assert base_playwright_module._parse_dimensions("") is None # noqa: SLF001
|
|
196
|
+
assert base_playwright_module._parse_dimensions(" 1440x900 ") == ( # noqa: SLF001
|
|
197
|
+
1440,
|
|
198
|
+
900,
|
|
199
|
+
)
|
|
200
|
+
assert base_playwright_module._parse_dimensions("1440X900") == ( # noqa: SLF001
|
|
201
|
+
1440,
|
|
202
|
+
900,
|
|
203
|
+
)
|
|
204
|
+
assert base_playwright_module._parse_dimensions("1600, 900") == ( # noqa: SLF001
|
|
205
|
+
1600,
|
|
206
|
+
900,
|
|
207
|
+
)
|
|
208
|
+
assert base_playwright_module._parse_dimensions("1600:900") is None # noqa: SLF001
|
|
209
|
+
assert base_playwright_module._parse_dimensions("1x2x3") is None # noqa: SLF001
|
|
210
|
+
assert base_playwright_module._parse_dimensions("1,2,3") is None # noqa: SLF001
|
|
211
|
+
assert base_playwright_module._parse_dimensions("wide x 900") is None # noqa: SLF001
|
|
212
|
+
|
|
213
|
+
monkeypatch.delenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", raising=False)
|
|
214
|
+
assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
|
|
215
|
+
1440,
|
|
216
|
+
900,
|
|
217
|
+
)
|
|
218
|
+
monkeypatch.setenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", "1600x900")
|
|
219
|
+
assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
|
|
220
|
+
1600,
|
|
221
|
+
900,
|
|
222
|
+
)
|
|
223
|
+
monkeypatch.setenv("MESHAGENT_PLAYWRIGHT_DIMENSIONS", "1200x800")
|
|
224
|
+
assert base_playwright_module._resolve_playwright_dimensions() == ( # noqa: SLF001
|
|
225
|
+
1440,
|
|
226
|
+
900,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
default_computer = BasePlaywrightComputer(starting_url=" ")
|
|
230
|
+
assert default_computer.dimensions == (1440, 900)
|
|
231
|
+
assert default_computer.starting_url == "https://google.com"
|
|
232
|
+
custom_computer = BasePlaywrightComputer(
|
|
233
|
+
dimensions=(1600, 900),
|
|
234
|
+
starting_url=" https://example.test ",
|
|
235
|
+
)
|
|
236
|
+
assert custom_computer.dimensions == (1600, 900)
|
|
237
|
+
assert custom_computer.starting_url == " https://example.test "
|
|
238
|
+
with pytest.raises(
|
|
239
|
+
ValueError,
|
|
240
|
+
match=r"playwright dimensions must be one of: \(1440, 900\), \(1600, 900\)",
|
|
241
|
+
):
|
|
242
|
+
BasePlaywrightComputer(dimensions=(1200, 800))
|
|
243
|
+
|
|
244
|
+
for input_key, expected in [
|
|
245
|
+
("/", "Divide"),
|
|
246
|
+
("\\", "Backslash"),
|
|
247
|
+
("alt", "Alt"),
|
|
248
|
+
("arrowdown", "ArrowDown"),
|
|
249
|
+
("arrowleft", "ArrowLeft"),
|
|
250
|
+
("arrowright", "ArrowRight"),
|
|
251
|
+
("arrowup", "ArrowUp"),
|
|
252
|
+
("backspace", "Backspace"),
|
|
253
|
+
("capslock", "CapsLock"),
|
|
254
|
+
("cmd", "Meta"),
|
|
255
|
+
("ctrl", "Control"),
|
|
256
|
+
("delete", "Delete"),
|
|
257
|
+
("end", "End"),
|
|
258
|
+
("enter", "Enter"),
|
|
259
|
+
("esc", "Escape"),
|
|
260
|
+
("home", "Home"),
|
|
261
|
+
("insert", "Insert"),
|
|
262
|
+
("option", "Alt"),
|
|
263
|
+
("pagedown", "PageDown"),
|
|
264
|
+
("pageup", "PageUp"),
|
|
265
|
+
("shift", "Shift"),
|
|
266
|
+
("space", " "),
|
|
267
|
+
("super", "Meta"),
|
|
268
|
+
("tab", "Tab"),
|
|
269
|
+
("win", "Meta"),
|
|
270
|
+
("CTRL", "Control"),
|
|
271
|
+
("A", "A"),
|
|
272
|
+
]:
|
|
273
|
+
assert (
|
|
274
|
+
base_playwright_module.CUA_KEY_TO_PLAYWRIGHT_KEY.get(
|
|
275
|
+
input_key.lower(),
|
|
276
|
+
input_key,
|
|
277
|
+
)
|
|
278
|
+
== expected
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
70
282
|
@pytest.mark.asyncio
|
|
71
283
|
async def test_computer_tool_emits_startup_progress_events():
|
|
72
284
|
computer = _FakeComputer()
|
|
@@ -148,6 +360,172 @@ async def test_computer_tool_propagates_computer_startup_events_via_tool_context
|
|
|
148
360
|
assert events[2]["state"] == "completed"
|
|
149
361
|
|
|
150
362
|
|
|
363
|
+
@pytest.mark.asyncio
|
|
364
|
+
async def test_computer_tool_startup_failure_event_matches_python_suppression():
|
|
365
|
+
class _FailingComputer(_FakeComputer):
|
|
366
|
+
def __init__(self, emitted_state: str | None) -> None:
|
|
367
|
+
super().__init__()
|
|
368
|
+
self.emitted_state = emitted_state
|
|
369
|
+
|
|
370
|
+
async def __aenter__(
|
|
371
|
+
self,
|
|
372
|
+
context: ComputerContext,
|
|
373
|
+
):
|
|
374
|
+
self.enter_contexts.append(context)
|
|
375
|
+
if self.emitted_state is not None:
|
|
376
|
+
context.emit_startup(state=self.emitted_state)
|
|
377
|
+
raise RuntimeError("boom")
|
|
378
|
+
|
|
379
|
+
for emitted_state, expected_states in (
|
|
380
|
+
(None, ["in_progress", "failed"]),
|
|
381
|
+
("failed", ["in_progress", "failed"]),
|
|
382
|
+
("cancelled", ["in_progress", "cancelled"]),
|
|
383
|
+
):
|
|
384
|
+
computer = _FailingComputer(emitted_state=emitted_state)
|
|
385
|
+
room = _FakeRoom(name="agent")
|
|
386
|
+
toolkit = ComputerToolkit(
|
|
387
|
+
computer=computer,
|
|
388
|
+
operator=_FakeOperator(),
|
|
389
|
+
room=room,
|
|
390
|
+
render_screen=None,
|
|
391
|
+
)
|
|
392
|
+
events: list[dict[str, Any]] = []
|
|
393
|
+
context = ToolContext(
|
|
394
|
+
caller=room.local_participant,
|
|
395
|
+
event_handler=events.append,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
computer_tool = next(
|
|
399
|
+
tool for tool in toolkit.tools if tool.name == "computer_call"
|
|
400
|
+
)
|
|
401
|
+
with pytest.raises(RuntimeError, match="boom"):
|
|
402
|
+
await computer_tool.handle_computer_call(
|
|
403
|
+
context=context,
|
|
404
|
+
type="computer_call",
|
|
405
|
+
action={"type": "wait"},
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
assert [event["state"] for event in events] == expected_states
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.asyncio
|
|
412
|
+
async def test_screenshot_tool_execute_matches_python_side_effects():
|
|
413
|
+
class _ScreenshotComputer(_FakeComputer):
|
|
414
|
+
def __init__(self) -> None:
|
|
415
|
+
super().__init__()
|
|
416
|
+
self.screenshot_calls: list[dict[str, Any]] = []
|
|
417
|
+
|
|
418
|
+
async def screenshot_bytes(
|
|
419
|
+
self,
|
|
420
|
+
context: ComputerContext,
|
|
421
|
+
*,
|
|
422
|
+
full_page: bool,
|
|
423
|
+
) -> bytes:
|
|
424
|
+
self.screenshot_calls.append(
|
|
425
|
+
{"context": context, "full_page": full_page},
|
|
426
|
+
)
|
|
427
|
+
return b"png-bytes"
|
|
428
|
+
|
|
429
|
+
computer = _ScreenshotComputer()
|
|
430
|
+
room = _FakeRoomWithStorage(name="agent")
|
|
431
|
+
toolkit = ComputerToolkit(
|
|
432
|
+
computer=computer,
|
|
433
|
+
operator=_FakeOperator(),
|
|
434
|
+
room=room,
|
|
435
|
+
render_screen=None,
|
|
436
|
+
)
|
|
437
|
+
events: list[dict[str, Any]] = []
|
|
438
|
+
context = ToolContext(caller=room.local_participant, event_handler=events.append)
|
|
439
|
+
tool = agent_module.ScreenshotTool(
|
|
440
|
+
room=room,
|
|
441
|
+
computer=computer,
|
|
442
|
+
toolkit=toolkit,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
result = await tool.execute(context=context, save_path="screen.png", full_page=True)
|
|
446
|
+
|
|
447
|
+
assert result == "saved screenshot to screen.png"
|
|
448
|
+
assert len(computer.enter_contexts) == 1
|
|
449
|
+
assert computer.screenshot_calls == [
|
|
450
|
+
{"context": computer.enter_contexts[0], "full_page": True},
|
|
451
|
+
]
|
|
452
|
+
assert room.storage.uploads == [
|
|
453
|
+
{"path": "screen.png", "data": b"png-bytes", "overwrite": True},
|
|
454
|
+
]
|
|
455
|
+
assert [event["state"] for event in events] == ["in_progress", "completed"]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@pytest.mark.asyncio
|
|
459
|
+
async def test_goto_tool_execute_matches_python_navigation_and_rendering():
|
|
460
|
+
class _GotoComputer(_FakeComputer):
|
|
461
|
+
def __init__(self) -> None:
|
|
462
|
+
super().__init__()
|
|
463
|
+
self.goto_calls: list[dict[str, Any]] = []
|
|
464
|
+
self.screenshot_calls: list[dict[str, Any]] = []
|
|
465
|
+
|
|
466
|
+
async def goto(
|
|
467
|
+
self,
|
|
468
|
+
context: ComputerContext,
|
|
469
|
+
*,
|
|
470
|
+
url: str,
|
|
471
|
+
) -> None:
|
|
472
|
+
self.goto_calls.append({"context": context, "url": url})
|
|
473
|
+
|
|
474
|
+
async def screenshot_bytes(
|
|
475
|
+
self,
|
|
476
|
+
context: ComputerContext,
|
|
477
|
+
*,
|
|
478
|
+
full_page: bool,
|
|
479
|
+
) -> bytes:
|
|
480
|
+
self.screenshot_calls.append(
|
|
481
|
+
{"context": context, "full_page": full_page},
|
|
482
|
+
)
|
|
483
|
+
return b"rendered-png"
|
|
484
|
+
|
|
485
|
+
computer = _GotoComputer()
|
|
486
|
+
room = _FakeRoom(name="agent")
|
|
487
|
+
rendered: list[bytes] = []
|
|
488
|
+
render_called = asyncio.Event()
|
|
489
|
+
|
|
490
|
+
async def render_screen(data: bytes) -> None:
|
|
491
|
+
rendered.append(data)
|
|
492
|
+
render_called.set()
|
|
493
|
+
|
|
494
|
+
toolkit = ComputerToolkit(
|
|
495
|
+
computer=computer,
|
|
496
|
+
operator=_FakeOperator(),
|
|
497
|
+
room=room,
|
|
498
|
+
render_screen=render_screen,
|
|
499
|
+
)
|
|
500
|
+
events: list[dict[str, Any]] = []
|
|
501
|
+
context = ToolContext(caller=room.local_participant, event_handler=events.append)
|
|
502
|
+
tool = agent_module.GotoURL(
|
|
503
|
+
computer=computer,
|
|
504
|
+
toolkit=toolkit,
|
|
505
|
+
render_screen=render_screen,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
result = await tool.execute(context=context, url="example.test/path")
|
|
509
|
+
|
|
510
|
+
assert result is None
|
|
511
|
+
assert render_called.is_set()
|
|
512
|
+
assert rendered == [b"rendered-png"]
|
|
513
|
+
assert len(computer.enter_contexts) == 1
|
|
514
|
+
assert computer.goto_calls == [
|
|
515
|
+
{
|
|
516
|
+
"context": computer.enter_contexts[0],
|
|
517
|
+
"url": "https://example.test/path",
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
assert computer.screenshot_calls == [
|
|
521
|
+
{"context": computer.enter_contexts[0], "full_page": False},
|
|
522
|
+
]
|
|
523
|
+
assert [event["state"] for event in events] == ["in_progress", "completed"]
|
|
524
|
+
|
|
525
|
+
await tool.execute(context=context, url="http://example.test/next")
|
|
526
|
+
assert computer.goto_calls[-1]["url"] == "http://example.test/next"
|
|
527
|
+
|
|
528
|
+
|
|
151
529
|
@pytest.mark.asyncio
|
|
152
530
|
async def test_computer_tool_render_screen_uses_python_lenient_base64_decode():
|
|
153
531
|
class _OutputOperator:
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from meshagent.computers import browserbase as browserbase_module
|
|
6
|
+
from meshagent.computers.browserbase import BrowserbaseBrowser
|
|
7
|
+
from meshagent.computers.computer import ComputerContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _FakeParticipant:
|
|
11
|
+
def get_attribute(self, key: str):
|
|
12
|
+
del key
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _FakeRoom:
|
|
17
|
+
local_participant = _FakeParticipant()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _FakeBrowserbaseSessions:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.create_calls: list[dict[str, object]] = []
|
|
23
|
+
|
|
24
|
+
async def create(self, **kwargs):
|
|
25
|
+
self.create_calls.append(kwargs)
|
|
26
|
+
return SimpleNamespace(id="session-1", connect_url="wss://connect.test")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _FakeBrowserbase:
|
|
30
|
+
instances: list["_FakeBrowserbase"] = []
|
|
31
|
+
|
|
32
|
+
def __init__(self, *, api_key: str | None = None):
|
|
33
|
+
self.api_key = api_key
|
|
34
|
+
self.sessions = _FakeBrowserbaseSessions()
|
|
35
|
+
self.instances.append(self)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _FakePage:
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self.goto_calls: list[str] = []
|
|
41
|
+
self.handlers: list[tuple[str, object]] = []
|
|
42
|
+
|
|
43
|
+
def on(self, event: str, handler) -> None:
|
|
44
|
+
self.handlers.append((event, handler))
|
|
45
|
+
|
|
46
|
+
async def goto(self, url: str) -> None:
|
|
47
|
+
self.goto_calls.append(url)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _FakeBrowserContext:
|
|
51
|
+
def __init__(self, page: _FakePage) -> None:
|
|
52
|
+
self.pages = [page]
|
|
53
|
+
self.handlers: list[tuple[str, object]] = []
|
|
54
|
+
self.init_scripts: list[str] = []
|
|
55
|
+
|
|
56
|
+
def on(self, event: str, handler) -> None:
|
|
57
|
+
self.handlers.append((event, handler))
|
|
58
|
+
|
|
59
|
+
async def add_init_script(self, script: str) -> None:
|
|
60
|
+
self.init_scripts.append(script)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _FakeBrowser:
|
|
64
|
+
def __init__(self, context: _FakeBrowserContext) -> None:
|
|
65
|
+
self.contexts = [context]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _FakeChromium:
|
|
69
|
+
def __init__(self, browser: _FakeBrowser) -> None:
|
|
70
|
+
self.browser = browser
|
|
71
|
+
self.connect_calls: list[dict[str, object]] = []
|
|
72
|
+
|
|
73
|
+
async def connect_over_cdp(self, connect_url: str, *, timeout: int):
|
|
74
|
+
self.connect_calls.append({"connect_url": connect_url, "timeout": timeout})
|
|
75
|
+
return self.browser
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _make_context() -> ComputerContext:
|
|
79
|
+
room = _FakeRoom()
|
|
80
|
+
return ComputerContext(room=room, caller=room.local_participant)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.asyncio
|
|
84
|
+
async def test_browserbase_get_browser_and_page_matches_python_session_shape(
|
|
85
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
86
|
+
) -> None:
|
|
87
|
+
_FakeBrowserbase.instances.clear()
|
|
88
|
+
monkeypatch.setenv("BROWSERBASE_API_KEY", "api-key")
|
|
89
|
+
monkeypatch.setenv("BROWSERBASE_PROJECT_ID", "project-id")
|
|
90
|
+
monkeypatch.setattr(browserbase_module, "AsyncBrowserbase", _FakeBrowserbase)
|
|
91
|
+
|
|
92
|
+
page = _FakePage()
|
|
93
|
+
browser_context = _FakeBrowserContext(page)
|
|
94
|
+
browser = _FakeBrowser(browser_context)
|
|
95
|
+
chromium = _FakeChromium(browser)
|
|
96
|
+
|
|
97
|
+
computer = BrowserbaseBrowser(
|
|
98
|
+
width=1200,
|
|
99
|
+
height=700,
|
|
100
|
+
region="eu-central-1",
|
|
101
|
+
proxy=True,
|
|
102
|
+
virtual_mouse=True,
|
|
103
|
+
ad_blocker=True,
|
|
104
|
+
starting_url=" https://browserbase.test ",
|
|
105
|
+
)
|
|
106
|
+
computer._playwright = SimpleNamespace(chromium=chromium)
|
|
107
|
+
|
|
108
|
+
returned_browser, returned_page = await computer._get_browser_and_page(
|
|
109
|
+
_make_context()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert returned_browser is browser
|
|
113
|
+
assert returned_page is page
|
|
114
|
+
assert computer.session.id == "session-1"
|
|
115
|
+
assert _FakeBrowserbase.instances[0].api_key == "api-key"
|
|
116
|
+
assert _FakeBrowserbase.instances[0].sessions.create_calls == [
|
|
117
|
+
{
|
|
118
|
+
"project_id": "project-id",
|
|
119
|
+
"browser_settings": {
|
|
120
|
+
"viewport": {"width": 1200, "height": 700},
|
|
121
|
+
"blockAds": True,
|
|
122
|
+
},
|
|
123
|
+
"region": "eu-central-1",
|
|
124
|
+
"proxies": True,
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
assert chromium.connect_calls == [
|
|
128
|
+
{"connect_url": "wss://connect.test", "timeout": 60000}
|
|
129
|
+
]
|
|
130
|
+
assert browser_context.handlers == [("page", computer._handle_new_page)]
|
|
131
|
+
assert len(browser_context.init_scripts) == 1
|
|
132
|
+
assert page.handlers == [("close", computer._handle_page_close)]
|
|
133
|
+
assert page.goto_calls == [" https://browserbase.test "]
|
|
@@ -350,7 +350,7 @@ async def test_container_playwright_uses_custom_starting_url() -> None:
|
|
|
350
350
|
computer = ContainerPlaywrightComputer(
|
|
351
351
|
room=room,
|
|
352
352
|
headless=True,
|
|
353
|
-
starting_url="https://example.com",
|
|
353
|
+
starting_url=" https://example.com ",
|
|
354
354
|
)
|
|
355
355
|
|
|
356
356
|
async def _ensure_container(context: ComputerContext) -> str:
|
|
@@ -378,7 +378,7 @@ async def test_container_playwright_uses_custom_starting_url() -> None:
|
|
|
378
378
|
assert connected_browser is browser
|
|
379
379
|
assert connected_page is page
|
|
380
380
|
assert page.viewport_calls == [{"width": 1440, "height": 900}]
|
|
381
|
-
assert page.goto_calls == ["https://example.com"]
|
|
381
|
+
assert page.goto_calls == [" https://example.com "]
|
|
382
382
|
|
|
383
383
|
|
|
384
384
|
@pytest.mark.asyncio
|
|
@@ -63,8 +63,8 @@ class DockerComputer:
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Fetch display geometry
|
|
66
|
-
geometry =
|
|
67
|
-
f"DISPLAY={self.display} xdotool getdisplaygeometry"
|
|
66
|
+
geometry = (
|
|
67
|
+
await self._exec(f"DISPLAY={self.display} xdotool getdisplaygeometry")
|
|
68
68
|
).strip()
|
|
69
69
|
if geometry:
|
|
70
70
|
w, h = geometry.split()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from meshagent.computers import docker as docker_module
|
|
6
|
+
from meshagent.computers.docker import DockerComputer, _async_check_output
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_async_check_output_shell_executes_and_preserves_failure_fields():
|
|
11
|
+
assert await _async_check_output("printf stdout", shell=True) == b"stdout"
|
|
12
|
+
|
|
13
|
+
with pytest.raises(subprocess.CalledProcessError) as exc_info:
|
|
14
|
+
await _async_check_output(
|
|
15
|
+
"printf stdout; printf stderr >&2; exit 7",
|
|
16
|
+
shell=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
exc = exc_info.value
|
|
20
|
+
assert exc.returncode == 7
|
|
21
|
+
assert exc.cmd == ("printf stdout; printf stderr >&2; exit 7",)
|
|
22
|
+
assert exc.output == b"stdout"
|
|
23
|
+
assert exc.stderr == b"stderr"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_docker_computer_aenter_awaits_exec_before_stripping_geometry(
|
|
28
|
+
monkeypatch,
|
|
29
|
+
):
|
|
30
|
+
class Result:
|
|
31
|
+
stdout = "container-id\n"
|
|
32
|
+
|
|
33
|
+
run_calls = []
|
|
34
|
+
|
|
35
|
+
def fake_run(*args, **kwargs):
|
|
36
|
+
run_calls.append((args, kwargs))
|
|
37
|
+
return Result()
|
|
38
|
+
|
|
39
|
+
async def fake_exec(self, cmd):
|
|
40
|
+
exec_calls.append(cmd)
|
|
41
|
+
return "1600 900\n"
|
|
42
|
+
|
|
43
|
+
exec_calls = []
|
|
44
|
+
monkeypatch.setattr(docker_module.subprocess, "run", fake_run)
|
|
45
|
+
monkeypatch.setattr(DockerComputer, "_exec", fake_exec)
|
|
46
|
+
|
|
47
|
+
computer = DockerComputer()
|
|
48
|
+
result = await computer.__aenter__(context=object())
|
|
49
|
+
|
|
50
|
+
assert result is computer
|
|
51
|
+
assert computer.dimensions == (1600, 900)
|
|
52
|
+
assert run_calls == [
|
|
53
|
+
(
|
|
54
|
+
(
|
|
55
|
+
[
|
|
56
|
+
"docker",
|
|
57
|
+
"ps",
|
|
58
|
+
"-q",
|
|
59
|
+
"-f",
|
|
60
|
+
"name=cua-sample-app",
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
{"capture_output": True, "text": True},
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
assert exec_calls == ["DISPLAY=:99 xdotool getdisplaygeometry"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_docker_computer_commands_match_python_source(monkeypatch) -> None:
|
|
71
|
+
calls: list[tuple[tuple, dict]] = []
|
|
72
|
+
|
|
73
|
+
async def fake_check_output(*args, **kwargs):
|
|
74
|
+
calls.append((args, kwargs))
|
|
75
|
+
return b"ok"
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr(docker_module, "_async_check_output", fake_check_output)
|
|
78
|
+
computer = DockerComputer()
|
|
79
|
+
|
|
80
|
+
assert await computer._exec('echo "hi"') == "ok"
|
|
81
|
+
assert calls.pop(0) == (
|
|
82
|
+
('docker exec cua-sample-app sh -c "echo \\"hi\\""',),
|
|
83
|
+
{"shell": True},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert await computer.screenshot(context=object()) == "ok"
|
|
87
|
+
assert calls.pop(0) == (
|
|
88
|
+
(
|
|
89
|
+
'docker exec cua-sample-app sh -c "export DISPLAY=:99 && import -window root png:- | base64 -w 0"',
|
|
90
|
+
),
|
|
91
|
+
{"shell": True},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
await computer.click(context=object(), x=10, y=20, button="middle")
|
|
95
|
+
await computer.click(context=object(), x=10, y=20, button="unknown")
|
|
96
|
+
await computer.double_click(context=object(), x=3, y=4)
|
|
97
|
+
await computer.scroll(context=object(), x=1, y=2, scroll_x=99, scroll_y=-2)
|
|
98
|
+
await computer.type(context=object(), text="don't")
|
|
99
|
+
await computer.move(context=object(), x=5, y=6)
|
|
100
|
+
await computer.keypress(context=object(), keys=["ENTER", "SPACE", "x"])
|
|
101
|
+
|
|
102
|
+
assert calls == [
|
|
103
|
+
(
|
|
104
|
+
(
|
|
105
|
+
'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 10 20 click 2"',
|
|
106
|
+
),
|
|
107
|
+
{"shell": True},
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
(
|
|
111
|
+
'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 10 20 click 1"',
|
|
112
|
+
),
|
|
113
|
+
{"shell": True},
|
|
114
|
+
),
|
|
115
|
+
(
|
|
116
|
+
(
|
|
117
|
+
'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 3 4 click --repeat 2 1"',
|
|
118
|
+
),
|
|
119
|
+
{"shell": True},
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 1 2"',),
|
|
123
|
+
{"shell": True},
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool click 4"',),
|
|
127
|
+
{"shell": True},
|
|
128
|
+
),
|
|
129
|
+
(
|
|
130
|
+
('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool click 4"',),
|
|
131
|
+
{"shell": True},
|
|
132
|
+
),
|
|
133
|
+
(
|
|
134
|
+
(
|
|
135
|
+
"docker exec cua-sample-app sh -c \"DISPLAY=:99 xdotool type -- 'don'\\''t'\"",
|
|
136
|
+
),
|
|
137
|
+
{"shell": True},
|
|
138
|
+
),
|
|
139
|
+
(
|
|
140
|
+
('docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool mousemove 5 6"',),
|
|
141
|
+
{"shell": True},
|
|
142
|
+
),
|
|
143
|
+
(
|
|
144
|
+
(
|
|
145
|
+
'docker exec cua-sample-app sh -c "DISPLAY=:99 xdotool key Return+space+x"',
|
|
146
|
+
),
|
|
147
|
+
{"shell": True},
|
|
148
|
+
),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
async def test_docker_computer_unsupported_url_methods_raise_runtime_error() -> None:
|
|
154
|
+
computer = DockerComputer()
|
|
155
|
+
|
|
156
|
+
with pytest.raises(
|
|
157
|
+
RuntimeError, match="get_current_url is not supported by DockerComputer"
|
|
158
|
+
):
|
|
159
|
+
await computer.get_current_url(context=object())
|
|
160
|
+
with pytest.raises(RuntimeError, match="goto is not supported by DockerComputer"):
|
|
161
|
+
await computer.goto(context=object(), url="https://example.test")
|
|
162
|
+
with pytest.raises(RuntimeError, match="back is not supported by DockerComputer"):
|
|
163
|
+
await computer.back(context=object())
|
|
164
|
+
with pytest.raises(
|
|
165
|
+
RuntimeError, match="forward is not supported by DockerComputer"
|
|
166
|
+
):
|
|
167
|
+
await computer.forward(context=object())
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/stagehand_test.py
RENAMED
|
@@ -61,6 +61,41 @@ class _FakeChromium:
|
|
|
61
61
|
return self.browser
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class _FakeRemoteBrowserContext:
|
|
65
|
+
def __init__(self, page: _FakePage | None = None) -> None:
|
|
66
|
+
self.pages = [] if page is None else [page]
|
|
67
|
+
self.new_context_calls: list[dict[str, object]] = []
|
|
68
|
+
self.new_page_calls = 0
|
|
69
|
+
self.page = page or _FakePage()
|
|
70
|
+
|
|
71
|
+
async def new_page(self) -> _FakePage:
|
|
72
|
+
self.new_page_calls += 1
|
|
73
|
+
self.pages.append(self.page)
|
|
74
|
+
return self.page
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _FakeRemoteBrowser:
|
|
78
|
+
def __init__(self, context: _FakeRemoteBrowserContext | None = None) -> None:
|
|
79
|
+
self.contexts = [] if context is None else [context]
|
|
80
|
+
self.context = context or _FakeRemoteBrowserContext()
|
|
81
|
+
self.new_context_calls: list[dict[str, object]] = []
|
|
82
|
+
|
|
83
|
+
async def new_context(self, **kwargs) -> _FakeRemoteBrowserContext:
|
|
84
|
+
self.new_context_calls.append(kwargs)
|
|
85
|
+
self.contexts.append(self.context)
|
|
86
|
+
return self.context
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _FakeRemoteChromium:
|
|
90
|
+
def __init__(self, browser: _FakeRemoteBrowser) -> None:
|
|
91
|
+
self.browser = browser
|
|
92
|
+
self.connect_calls: list[dict[str, object]] = []
|
|
93
|
+
|
|
94
|
+
async def connect_over_cdp(self, cdp_url: str, **kwargs):
|
|
95
|
+
self.connect_calls.append({"cdp_url": cdp_url, **kwargs})
|
|
96
|
+
return self.browser
|
|
97
|
+
|
|
98
|
+
|
|
64
99
|
class _FakeStagehandSessions:
|
|
65
100
|
def __init__(self) -> None:
|
|
66
101
|
self.start_calls: list[dict[str, object]] = []
|
|
@@ -126,7 +161,7 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
|
|
|
126
161
|
|
|
127
162
|
computer = StagehandComputer(
|
|
128
163
|
dimensions=(1600, 900),
|
|
129
|
-
starting_url="https://example.com",
|
|
164
|
+
starting_url=" https://example.com ",
|
|
130
165
|
)
|
|
131
166
|
computer._playwright = SimpleNamespace(chromium=chromium)
|
|
132
167
|
|
|
@@ -151,7 +186,7 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
|
|
|
151
186
|
assert page.viewport_calls == [{"width": 1600, "height": 900}]
|
|
152
187
|
assert page.goto_calls == [
|
|
153
188
|
("about:blank", "domcontentloaded"),
|
|
154
|
-
("https://example.com", None),
|
|
189
|
+
(" https://example.com ", None),
|
|
155
190
|
]
|
|
156
191
|
|
|
157
192
|
assert len(_FakeStagehand.init_calls) == 1
|
|
@@ -188,6 +223,67 @@ async def test_stagehand_computer_uses_room_runtime_for_local_stagehand(
|
|
|
188
223
|
assert stagehand.closed is True
|
|
189
224
|
|
|
190
225
|
|
|
226
|
+
@pytest.mark.asyncio
|
|
227
|
+
async def test_stagehand_computer_remote_session_plan_matches_python(
|
|
228
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
229
|
+
) -> None:
|
|
230
|
+
_FakeStagehand.init_calls.clear()
|
|
231
|
+
remote_browser = _FakeRemoteBrowser()
|
|
232
|
+
chromium = _FakeRemoteChromium(browser=remote_browser)
|
|
233
|
+
|
|
234
|
+
monkeypatch.setattr(
|
|
235
|
+
stagehand_module, "_require_stagehand_class", lambda: _FakeStagehand
|
|
236
|
+
)
|
|
237
|
+
monkeypatch.setattr(
|
|
238
|
+
stagehand_module,
|
|
239
|
+
"stagehand_available",
|
|
240
|
+
lambda **_: True,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
computer = StagehandComputer(
|
|
244
|
+
dimensions=(1600, 900),
|
|
245
|
+
starting_url="https://remote.example",
|
|
246
|
+
stagehand_config=StagehandComputerConfig(
|
|
247
|
+
server="remote",
|
|
248
|
+
browser={"type": "browserbase"},
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
computer._playwright = SimpleNamespace(chromium=chromium)
|
|
252
|
+
|
|
253
|
+
browser, page = await computer._get_browser_and_page(_make_context())
|
|
254
|
+
|
|
255
|
+
assert browser is remote_browser
|
|
256
|
+
assert page is remote_browser.context.page
|
|
257
|
+
assert chromium.connect_calls == [
|
|
258
|
+
{
|
|
259
|
+
"cdp_url": "ws://127.0.0.1:9222/devtools/browser/example",
|
|
260
|
+
"timeout": 60_000,
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
assert remote_browser.new_context_calls == [
|
|
264
|
+
{"viewport": {"width": 1600, "height": 900}}
|
|
265
|
+
]
|
|
266
|
+
assert remote_browser.context.new_page_calls == 1
|
|
267
|
+
assert page.viewport_calls == [{"width": 1600, "height": 900}]
|
|
268
|
+
assert page.goto_calls == [("https://remote.example", None)]
|
|
269
|
+
|
|
270
|
+
assert len(_FakeStagehand.init_calls) == 1
|
|
271
|
+
init_call = _FakeStagehand.init_calls[0]
|
|
272
|
+
assert init_call["server"] == "remote"
|
|
273
|
+
assert init_call["model_api_key"] == "room_token"
|
|
274
|
+
assert init_call["local_openai_api_key"] == "room_token"
|
|
275
|
+
|
|
276
|
+
stagehand = computer._stagehand
|
|
277
|
+
assert isinstance(stagehand, _FakeStagehand)
|
|
278
|
+
assert stagehand.sessions.start_calls == [
|
|
279
|
+
{
|
|
280
|
+
"model_name": "openai/gpt-5.4",
|
|
281
|
+
"browser": {"type": "browserbase"},
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
assert computer._stagehand_session_id == "stagehand_session_1"
|
|
285
|
+
|
|
286
|
+
|
|
191
287
|
def test_stagehand_computer_can_update_config() -> None:
|
|
192
288
|
computer = StagehandComputer()
|
|
193
289
|
|
|
@@ -202,6 +298,49 @@ def test_stagehand_computer_can_update_config() -> None:
|
|
|
202
298
|
)
|
|
203
299
|
|
|
204
300
|
|
|
301
|
+
def test_stagehand_start_kwargs_and_update_conflict_match_python() -> None:
|
|
302
|
+
computer = StagehandComputer(
|
|
303
|
+
stagehand_config=StagehandComputerConfig(
|
|
304
|
+
model_name="openai/custom",
|
|
305
|
+
browserbase_session_create_params={"keepAlive": True},
|
|
306
|
+
browserbase_session_id="session-1",
|
|
307
|
+
dom_settle_timeout_ms=1234.0,
|
|
308
|
+
experimental=True,
|
|
309
|
+
self_heal=False,
|
|
310
|
+
system_prompt="system",
|
|
311
|
+
verbose=2,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
assert computer._runtime_stagehand_start_kwargs(browser={"type": "remote"}) == {
|
|
316
|
+
"model_name": "openai/custom",
|
|
317
|
+
"browser": {"type": "remote"},
|
|
318
|
+
"browserbase_session_create_params": {"keepAlive": True},
|
|
319
|
+
"browserbase_session_id": "session-1",
|
|
320
|
+
"dom_settle_timeout_ms": 1234.0,
|
|
321
|
+
"experimental": True,
|
|
322
|
+
"self_heal": False,
|
|
323
|
+
"system_prompt": "system",
|
|
324
|
+
"verbose": 2,
|
|
325
|
+
}
|
|
326
|
+
assert computer._runtime_stagehand_start_kwargs(browser=None) == {
|
|
327
|
+
"model_name": "openai/custom",
|
|
328
|
+
"browserbase_session_create_params": {"keepAlive": True},
|
|
329
|
+
"browserbase_session_id": "session-1",
|
|
330
|
+
"dom_settle_timeout_ms": 1234.0,
|
|
331
|
+
"experimental": True,
|
|
332
|
+
"self_heal": False,
|
|
333
|
+
"system_prompt": "system",
|
|
334
|
+
"verbose": 2,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
with pytest.raises(ValueError, match="pass config or keyword changes, not both"):
|
|
338
|
+
computer.update_stagehand_config(
|
|
339
|
+
config=StagehandComputerConfig(),
|
|
340
|
+
model_name="openai/other",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
205
344
|
def test_stagehand_local_browser_config_uses_python_dict_coercion() -> None:
|
|
206
345
|
computer = StagehandComputer(
|
|
207
346
|
stagehand_config=StagehandComputerConfig(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from meshagent.computers import utils as utils_module
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
"url",
|
|
10
|
+
[
|
|
11
|
+
"https://maliciousbook.com",
|
|
12
|
+
"https://login.maliciousbook.com/path",
|
|
13
|
+
"https://evilvideos.com/watch?v=1",
|
|
14
|
+
"https://EVILVIDEOS.COM/watch?v=1",
|
|
15
|
+
"https://user:pass@evilvideos.com:443/path",
|
|
16
|
+
"https://user@sub.ilanbigio.com/path",
|
|
17
|
+
"https://evilvideos.com:bad/path",
|
|
18
|
+
"//evilvideos.com/path",
|
|
19
|
+
],
|
|
20
|
+
)
|
|
21
|
+
def test_check_blocklisted_url_rejects_blocklisted_domains(url: str) -> None:
|
|
22
|
+
with pytest.raises(ValueError, match="Blocked URL"):
|
|
23
|
+
utils_module.check_blocklisted_url(url)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_check_blocklisted_url_allows_other_domains() -> None:
|
|
27
|
+
utils_module.check_blocklisted_url("https://example.com/path")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize(
|
|
31
|
+
"url",
|
|
32
|
+
[
|
|
33
|
+
"EVILVIDEOS.COM/path",
|
|
34
|
+
"evilvideos.com/path",
|
|
35
|
+
"http://evilvideos.com\\@example.com/path",
|
|
36
|
+
"http://sub.ilanbigio.com.",
|
|
37
|
+
"http:// evilvideos.com /path",
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
def test_check_blocklisted_url_preserves_urlparse_hostname_edges(url: str) -> None:
|
|
41
|
+
utils_module.check_blocklisted_url(url)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_check_blocklisted_url_raises_urlparse_errors() -> None:
|
|
45
|
+
with pytest.raises(ValueError, match="Invalid IPv6 URL"):
|
|
46
|
+
utils_module.check_blocklisted_url("http://[::1")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("value", [None, [], "x", 3])
|
|
50
|
+
def test_sanitize_message_non_dict_inputs_raise_python_get_error(value) -> None:
|
|
51
|
+
with pytest.raises(AttributeError, match="object has no attribute 'get'"):
|
|
52
|
+
utils_module.sanitize_message(value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_calculate_image_dimensions_supports_xbm_like_pillow() -> None:
|
|
56
|
+
xbm = (
|
|
57
|
+
b"#define sample_width 17\n"
|
|
58
|
+
b"#define sample_height 9\n"
|
|
59
|
+
b"static unsigned char sample_bits[] = { 0x00 };\n"
|
|
60
|
+
)
|
|
61
|
+
assert utils_module.calculate_image_dimensions(base64.b64encode(xbm).decode()) == (
|
|
62
|
+
17,
|
|
63
|
+
9,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_calculate_image_dimensions_supports_xpm_like_pillow() -> None:
|
|
68
|
+
xpm = (
|
|
69
|
+
b"/* XPM */\n"
|
|
70
|
+
b"static char * sample[] = {\n"
|
|
71
|
+
b'"13 7 1 1",\n'
|
|
72
|
+
b'"a c #000000",\n'
|
|
73
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
74
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
75
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
76
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
77
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
78
|
+
b'"aaaaaaaaaaaaa",\n'
|
|
79
|
+
b'"aaaaaaaaaaaaa"};\n'
|
|
80
|
+
)
|
|
81
|
+
assert utils_module.calculate_image_dimensions(base64.b64encode(xpm).decode()) == (
|
|
82
|
+
13,
|
|
83
|
+
7,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.parametrize(
|
|
88
|
+
"payload",
|
|
89
|
+
[
|
|
90
|
+
bytes([0, 0, 1, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 22, 0, 0, 0])
|
|
91
|
+
+ b"abcd",
|
|
92
|
+
bytes([0, 0, 2, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 22, 0, 0, 0])
|
|
93
|
+
+ b"abcd",
|
|
94
|
+
bytes([0, 0, 1, 0, 1, 0, 16, 10, 0, 0, 1, 0, 32, 0, 4, 0, 0, 0, 100, 0, 0, 0])
|
|
95
|
+
+ b"abcd",
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
def test_calculate_image_dimensions_rejects_invalid_ico_payloads_like_pillow(
|
|
99
|
+
payload: bytes,
|
|
100
|
+
) -> None:
|
|
101
|
+
with pytest.raises(Exception):
|
|
102
|
+
utils_module.calculate_image_dimensions(base64.b64encode(payload).decode())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.45.6"
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6/meshagent_computers.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-computers
|
|
3
|
-
Version: 0.45.
|
|
3
|
+
Version: 0.45.6
|
|
4
4
|
Summary: Computer Building Blocks for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -12,9 +12,9 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: pytest~=8.4
|
|
13
13
|
Requires-Dist: pytest-asyncio~=0.26
|
|
14
14
|
Requires-Dist: openai~=2.25.0
|
|
15
|
-
Requires-Dist: meshagent-api==0.45.
|
|
16
|
-
Requires-Dist: meshagent-agents==0.45.
|
|
17
|
-
Requires-Dist: meshagent-tools==0.45.
|
|
15
|
+
Requires-Dist: meshagent-api==0.45.6
|
|
16
|
+
Requires-Dist: meshagent-agents==0.45.6
|
|
17
|
+
Requires-Dist: meshagent-tools==0.45.6
|
|
18
18
|
Requires-Dist: playwright~=1.58.0
|
|
19
19
|
Requires-Dist: stagehand~=3.6.0
|
|
20
20
|
Requires-Dist: browserbase~=1.2
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/SOURCES.txt
RENAMED
|
@@ -8,6 +8,7 @@ meshagent/computers/agent.py
|
|
|
8
8
|
meshagent/computers/agent_test.py
|
|
9
9
|
meshagent/computers/base_playwright.py
|
|
10
10
|
meshagent/computers/browserbase.py
|
|
11
|
+
meshagent/computers/browserbase_test.py
|
|
11
12
|
meshagent/computers/computer.py
|
|
12
13
|
meshagent/computers/container_playwright.py
|
|
13
14
|
meshagent/computers/container_playwright_test.py
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/requires.txt
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
pytest~=8.4
|
|
2
2
|
pytest-asyncio~=0.26
|
|
3
3
|
openai~=2.25.0
|
|
4
|
-
meshagent-api==0.45.
|
|
5
|
-
meshagent-agents==0.45.
|
|
6
|
-
meshagent-tools==0.45.
|
|
4
|
+
meshagent-api==0.45.6
|
|
5
|
+
meshagent-agents==0.45.6
|
|
6
|
+
meshagent-tools==0.45.6
|
|
7
7
|
playwright~=1.58.0
|
|
8
8
|
stagehand~=3.6.0
|
|
9
9
|
browserbase~=1.2
|
|
@@ -15,9 +15,9 @@ dependencies = [
|
|
|
15
15
|
"pytest~=8.4",
|
|
16
16
|
"pytest-asyncio~=0.26",
|
|
17
17
|
"openai~=2.25.0",
|
|
18
|
-
"meshagent-api==0.45.
|
|
19
|
-
"meshagent-agents==0.45.
|
|
20
|
-
"meshagent-tools==0.45.
|
|
18
|
+
"meshagent-api==0.45.6",
|
|
19
|
+
"meshagent-agents==0.45.6",
|
|
20
|
+
"meshagent-tools==0.45.6",
|
|
21
21
|
"playwright~=1.58.0",
|
|
22
22
|
"stagehand~=3.6.0",
|
|
23
23
|
"browserbase~=1.2",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from meshagent.computers.docker import _async_check_output
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.mark.asyncio
|
|
9
|
-
async def test_async_check_output_shell_executes_and_preserves_failure_fields():
|
|
10
|
-
assert await _async_check_output("printf stdout", shell=True) == b"stdout"
|
|
11
|
-
|
|
12
|
-
with pytest.raises(subprocess.CalledProcessError) as exc_info:
|
|
13
|
-
await _async_check_output(
|
|
14
|
-
"printf stdout; printf stderr >&2; exit 7",
|
|
15
|
-
shell=True,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
exc = exc_info.value
|
|
19
|
-
assert exc.returncode == 7
|
|
20
|
-
assert exc.cmd == ("printf stdout; printf stderr >&2; exit 7",)
|
|
21
|
-
assert exc.output == b"stdout"
|
|
22
|
-
assert exc.stderr == b"stderr"
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from meshagent.computers import utils as utils_module
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.mark.parametrize(
|
|
7
|
-
"url",
|
|
8
|
-
[
|
|
9
|
-
"https://maliciousbook.com",
|
|
10
|
-
"https://login.maliciousbook.com/path",
|
|
11
|
-
"https://evilvideos.com/watch?v=1",
|
|
12
|
-
"https://user:pass@evilvideos.com:443/path",
|
|
13
|
-
"//evilvideos.com/path",
|
|
14
|
-
],
|
|
15
|
-
)
|
|
16
|
-
def test_check_blocklisted_url_rejects_blocklisted_domains(url: str) -> None:
|
|
17
|
-
with pytest.raises(ValueError, match="Blocked URL"):
|
|
18
|
-
utils_module.check_blocklisted_url(url)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_check_blocklisted_url_allows_other_domains() -> None:
|
|
22
|
-
utils_module.check_blocklisted_url("https://example.com/path")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@pytest.mark.parametrize(
|
|
26
|
-
"url",
|
|
27
|
-
[
|
|
28
|
-
"EVILVIDEOS.COM/path",
|
|
29
|
-
"evilvideos.com/path",
|
|
30
|
-
"http://evilvideos.com\\@example.com/path",
|
|
31
|
-
"http://sub.ilanbigio.com.",
|
|
32
|
-
"http:// evilvideos.com /path",
|
|
33
|
-
],
|
|
34
|
-
)
|
|
35
|
-
def test_check_blocklisted_url_preserves_urlparse_hostname_edges(url: str) -> None:
|
|
36
|
-
utils_module.check_blocklisted_url(url)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_check_blocklisted_url_raises_urlparse_errors() -> None:
|
|
40
|
-
with pytest.raises(ValueError, match="Invalid IPv6 URL"):
|
|
41
|
-
utils_module.check_blocklisted_url("http://[::1")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@pytest.mark.parametrize("value", [None, [], "x", 3])
|
|
45
|
-
def test_sanitize_message_non_dict_inputs_raise_python_get_error(value) -> None:
|
|
46
|
-
with pytest.raises(AttributeError, match="object has no attribute 'get'"):
|
|
47
|
-
utils_module.sanitize_message(value)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.45.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/base_playwright.py
RENAMED
|
File without changes
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/browserbase.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/local_playwright.py
RENAMED
|
File without changes
|
|
File without changes
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent/computers/operator_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{meshagent_computers-0.45.4 → meshagent_computers-0.45.6}/meshagent_computers.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|