gl-browser-use-binary 0.0.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gl_browser_use/__init__.py +60 -0
- gl_browser_use/__init__.pyi +12 -0
- gl_browser_use/client.py +639 -0
- gl_browser_use/client.pyi +57 -0
- gl_browser_use/config.py +169 -0
- gl_browser_use/config.pyi +42 -0
- gl_browser_use/errors.py +119 -0
- gl_browser_use/errors.pyi +40 -0
- gl_browser_use/events.py +147 -0
- gl_browser_use/events.pyi +29 -0
- gl_browser_use/infrastructure/__init__.py +39 -0
- gl_browser_use/infrastructure/__init__.pyi +6 -0
- gl_browser_use/infrastructure/base.py +103 -0
- gl_browser_use/infrastructure/base.pyi +56 -0
- gl_browser_use/infrastructure/steel.py +296 -0
- gl_browser_use/infrastructure/steel.pyi +56 -0
- gl_browser_use/models.py +45 -0
- gl_browser_use/models.pyi +32 -0
- gl_browser_use/parsing.py +264 -0
- gl_browser_use/parsing.pyi +41 -0
- gl_browser_use/parsing_recovery.py +204 -0
- gl_browser_use/parsing_recovery.pyi +35 -0
- gl_browser_use/session_errors.py +72 -0
- gl_browser_use/session_errors.pyi +16 -0
- gl_browser_use/storage/__init__.py +39 -0
- gl_browser_use/storage/__init__.pyi +6 -0
- gl_browser_use/storage/base.py +125 -0
- gl_browser_use/storage/base.pyi +46 -0
- gl_browser_use/storage/minio_compatible.py +84 -0
- gl_browser_use/storage/minio_compatible.pyi +15 -0
- gl_browser_use_binary-0.0.0b5.dist-info/METADATA +236 -0
- gl_browser_use_binary-0.0.0b5.dist-info/RECORD +34 -0
- gl_browser_use_binary-0.0.0b5.dist-info/WHEEL +5 -0
- gl_browser_use_binary-0.0.0b5.dist-info/top_level.txt +1 -0
gl_browser_use/client.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""Client implementation for GL Browser Use SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Reinhart Linanda (reinhart.linanda@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import dataclasses
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from browser_use import Agent
|
|
19
|
+
from browser_use.browser import BrowserSession
|
|
20
|
+
from browser_use.llm import ChatOpenAI
|
|
21
|
+
from browser_use.logging_config import setup_logging
|
|
22
|
+
from bubus import EventBus
|
|
23
|
+
from openai.types.shared_params.chat_model import ChatModel
|
|
24
|
+
from openai.types.shared_params.reasoning_effort import ReasoningEffort
|
|
25
|
+
|
|
26
|
+
from gl_browser_use.config import BrowserUseClientConfig
|
|
27
|
+
from gl_browser_use.errors import (
|
|
28
|
+
BrowserUseConfigurationError,
|
|
29
|
+
BrowserUseExecutionError,
|
|
30
|
+
BrowserUseRetryExhaustedError,
|
|
31
|
+
)
|
|
32
|
+
from gl_browser_use.events import (
|
|
33
|
+
RECORDING_MESSAGE,
|
|
34
|
+
STREAMING_MESSAGE,
|
|
35
|
+
TASK_COMPLETED_MESSAGE,
|
|
36
|
+
build_activity_event,
|
|
37
|
+
build_error_event,
|
|
38
|
+
build_retry_event,
|
|
39
|
+
build_step_content,
|
|
40
|
+
build_step_event,
|
|
41
|
+
build_thinking_event,
|
|
42
|
+
)
|
|
43
|
+
from gl_browser_use.infrastructure.base import (
|
|
44
|
+
BrowserInfrastructure,
|
|
45
|
+
BrowserInfrastructureContext,
|
|
46
|
+
)
|
|
47
|
+
from gl_browser_use.models import (
|
|
48
|
+
BrowserUseRunResult,
|
|
49
|
+
BrowserUseRunStatus,
|
|
50
|
+
BrowserUseStreamEvent,
|
|
51
|
+
)
|
|
52
|
+
from gl_browser_use.parsing import (
|
|
53
|
+
coerce_string,
|
|
54
|
+
extract_final_output,
|
|
55
|
+
extract_tool_calls,
|
|
56
|
+
summarize_terminal_error,
|
|
57
|
+
validate_structured_data_output,
|
|
58
|
+
)
|
|
59
|
+
from gl_browser_use.session_errors import is_recoverable_session_error
|
|
60
|
+
from gl_browser_use.storage.base import ObjectStorage
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
_AGENT_EVENTBUS_STOP_TIMEOUT_IN_S = 5.0
|
|
65
|
+
|
|
66
|
+
EmitCallable = Callable[[BrowserUseStreamEvent | None], Awaitable[None] | None]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _maybe_await(value: Any) -> None:
|
|
70
|
+
if inspect.isawaitable(value):
|
|
71
|
+
await value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def recording_metadata(
|
|
75
|
+
*,
|
|
76
|
+
infrastructure: BrowserInfrastructure | None,
|
|
77
|
+
storage: ObjectStorage | None,
|
|
78
|
+
browser_context: BrowserInfrastructureContext | None,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Return recording status metadata aligned with implementation behavior."""
|
|
81
|
+
if infrastructure is None:
|
|
82
|
+
return {
|
|
83
|
+
"recording_status": "disabled",
|
|
84
|
+
"recording_message": "Browser infrastructure is not configured.",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if not infrastructure.supports_recording:
|
|
88
|
+
return {
|
|
89
|
+
"recording_status": "unsupported",
|
|
90
|
+
"recording_message": f"Infrastructure '{infrastructure.provider}' does not support recording.",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if storage is None:
|
|
94
|
+
return {
|
|
95
|
+
"recording_status": "disabled",
|
|
96
|
+
"recording_message": "Object storage is not configured.",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if not storage.is_available:
|
|
100
|
+
return {
|
|
101
|
+
"recording_status": "unavailable",
|
|
102
|
+
"recording_message": f"Object storage {storage.provider} is not available: {getattr(storage, 'last_error', None)}",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if browser_context is None:
|
|
106
|
+
return {
|
|
107
|
+
"recording_status": "disabled",
|
|
108
|
+
"recording_message": "Browser context is not available.",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"recording_status": "scheduled",
|
|
113
|
+
"recording_message": "Recording has been scheduled as a background task.",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BrowserUseClient:
|
|
118
|
+
"""Facade for running browser automation tasks.
|
|
119
|
+
|
|
120
|
+
Note: ``infrastructure`` and ``storage`` attributes are read by ``run``/
|
|
121
|
+
``run_once``/``run_sync`` on each call. Swapping them while a run is in
|
|
122
|
+
flight is undefined behavior.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
config: BrowserUseClientConfig,
|
|
129
|
+
infrastructure: BrowserInfrastructure | None = None,
|
|
130
|
+
storage: ObjectStorage | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Initialize the client."""
|
|
133
|
+
config.validate(strict=True)
|
|
134
|
+
self.config = config
|
|
135
|
+
self.infrastructure = infrastructure
|
|
136
|
+
self.storage = storage
|
|
137
|
+
|
|
138
|
+
async def run(self, task: str) -> AsyncGenerator[BrowserUseStreamEvent, None]:
|
|
139
|
+
"""Run the client."""
|
|
140
|
+
queue: asyncio.Queue[BrowserUseStreamEvent | None] = asyncio.Queue()
|
|
141
|
+
emit_lock = asyncio.Lock()
|
|
142
|
+
|
|
143
|
+
async def emit(event: BrowserUseStreamEvent | None) -> None:
|
|
144
|
+
async with emit_lock:
|
|
145
|
+
await queue.put(event)
|
|
146
|
+
|
|
147
|
+
async def run_and_signal() -> BrowserUseRunResult:
|
|
148
|
+
try:
|
|
149
|
+
return await self._run_with_events(task, emit)
|
|
150
|
+
finally:
|
|
151
|
+
await emit(None)
|
|
152
|
+
|
|
153
|
+
run_task = asyncio.create_task(run_and_signal())
|
|
154
|
+
try:
|
|
155
|
+
while True:
|
|
156
|
+
event = await queue.get()
|
|
157
|
+
if event is None:
|
|
158
|
+
break
|
|
159
|
+
yield event
|
|
160
|
+
await run_task
|
|
161
|
+
finally:
|
|
162
|
+
if not run_task.done():
|
|
163
|
+
run_task.cancel()
|
|
164
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
165
|
+
await run_task
|
|
166
|
+
|
|
167
|
+
async def run_once(self, task: str) -> BrowserUseRunResult:
|
|
168
|
+
"""Run the client once and return a single aggregated result.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
------
|
|
172
|
+
BrowserUseExecutionError
|
|
173
|
+
When the task input is invalid.
|
|
174
|
+
BrowserUseConfigurationError
|
|
175
|
+
When required configuration is missing.
|
|
176
|
+
BrowserUseRetryExhaustedError
|
|
177
|
+
When all recoverable session retries are exhausted.
|
|
178
|
+
"""
|
|
179
|
+
events: list[BrowserUseStreamEvent] = []
|
|
180
|
+
|
|
181
|
+
async def emit(event: BrowserUseStreamEvent | None) -> None:
|
|
182
|
+
if event is not None:
|
|
183
|
+
events.append(event)
|
|
184
|
+
|
|
185
|
+
result = await self._run_with_events(task, emit)
|
|
186
|
+
return dataclasses.replace(result, events=events)
|
|
187
|
+
|
|
188
|
+
def run_sync(self, task: str) -> BrowserUseRunResult:
|
|
189
|
+
"""Run the client synchronously.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
------
|
|
193
|
+
BrowserUseExecutionError
|
|
194
|
+
When invoked from a thread that already has a running event loop,
|
|
195
|
+
or when task validation fails.
|
|
196
|
+
BrowserUseConfigurationError
|
|
197
|
+
When required configuration is missing.
|
|
198
|
+
BrowserUseRetryExhaustedError
|
|
199
|
+
When all recoverable session retries are exhausted.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
asyncio.get_running_loop()
|
|
203
|
+
except RuntimeError:
|
|
204
|
+
pass
|
|
205
|
+
else:
|
|
206
|
+
raise BrowserUseExecutionError(
|
|
207
|
+
"run_sync must not be called from a running event loop"
|
|
208
|
+
)
|
|
209
|
+
return asyncio.run(self.run_once(task))
|
|
210
|
+
|
|
211
|
+
async def _run_with_events(
|
|
212
|
+
self, task: str, emit: EmitCallable
|
|
213
|
+
) -> BrowserUseRunResult:
|
|
214
|
+
task = self._validate_task(task)
|
|
215
|
+
max_attempts = self.config.max_session_retries + 1
|
|
216
|
+
|
|
217
|
+
for attempt in range(1, max_attempts + 1):
|
|
218
|
+
try:
|
|
219
|
+
result = await self._run_single_attempt(task, emit, attempt=attempt)
|
|
220
|
+
await _maybe_await(emit(None))
|
|
221
|
+
return dataclasses.replace(
|
|
222
|
+
result,
|
|
223
|
+
metadata={**result.metadata, "max_attempts": max_attempts},
|
|
224
|
+
)
|
|
225
|
+
except BrowserUseExecutionError as exc:
|
|
226
|
+
raw_reason = str(exc)
|
|
227
|
+
display_reason = summarize_terminal_error(raw_reason)
|
|
228
|
+
recoverable = is_recoverable_session_error(raw_reason)
|
|
229
|
+
|
|
230
|
+
if recoverable and attempt < max_attempts:
|
|
231
|
+
await _maybe_await(
|
|
232
|
+
emit(build_retry_event(attempt, max_attempts, display_reason))
|
|
233
|
+
)
|
|
234
|
+
delay = max(0.0, self.config.session_retry_delay_in_s)
|
|
235
|
+
if delay:
|
|
236
|
+
await asyncio.sleep(delay)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
240
|
+
recording_url_from_details = coerce_string(details.get("recording_url"))
|
|
241
|
+
resolved_recording_url = (
|
|
242
|
+
recording_url_from_details
|
|
243
|
+
or self._resolve_recording_url_from_details(details)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await _maybe_await(
|
|
247
|
+
emit(
|
|
248
|
+
build_error_event(
|
|
249
|
+
display_reason, recording_url=resolved_recording_url or ""
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
await _maybe_await(emit(None))
|
|
254
|
+
|
|
255
|
+
if recoverable:
|
|
256
|
+
raise BrowserUseRetryExhaustedError(
|
|
257
|
+
f"Session retries exhausted after {max_attempts} attempts: {display_reason}",
|
|
258
|
+
attempt=attempt,
|
|
259
|
+
max_attempts=max_attempts,
|
|
260
|
+
details={
|
|
261
|
+
"streaming_url": details.get("streaming_url"),
|
|
262
|
+
"recording_url": resolved_recording_url,
|
|
263
|
+
},
|
|
264
|
+
origin=exc,
|
|
265
|
+
) from exc
|
|
266
|
+
|
|
267
|
+
return self._build_terminal_result(
|
|
268
|
+
task=task,
|
|
269
|
+
attempt=attempt,
|
|
270
|
+
max_attempts=max_attempts,
|
|
271
|
+
reason=display_reason,
|
|
272
|
+
details=details,
|
|
273
|
+
resolved_recording_url=resolved_recording_url,
|
|
274
|
+
)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
display_reason = summarize_terminal_error(str(exc))
|
|
277
|
+
await _maybe_await(emit(build_error_event(display_reason)))
|
|
278
|
+
await _maybe_await(emit(None))
|
|
279
|
+
return self._build_terminal_result(
|
|
280
|
+
task=task,
|
|
281
|
+
attempt=attempt,
|
|
282
|
+
max_attempts=max_attempts,
|
|
283
|
+
reason=display_reason,
|
|
284
|
+
details={},
|
|
285
|
+
resolved_recording_url=None,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Defensive: the loop always returns or raises; this guard keeps
|
|
289
|
+
# type-checkers happy about the return contract.
|
|
290
|
+
await _maybe_await(emit(None))
|
|
291
|
+
raise BrowserUseRetryExhaustedError(
|
|
292
|
+
"run attempt loop exhausted unexpectedly",
|
|
293
|
+
attempt=max_attempts,
|
|
294
|
+
max_attempts=max_attempts,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _build_terminal_result(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
task: str,
|
|
301
|
+
attempt: int,
|
|
302
|
+
max_attempts: int,
|
|
303
|
+
reason: str,
|
|
304
|
+
details: dict[str, Any],
|
|
305
|
+
resolved_recording_url: str | None,
|
|
306
|
+
) -> BrowserUseRunResult:
|
|
307
|
+
"""Build a terminal error :class:`BrowserUseRunResult` from attempt metadata.
|
|
308
|
+
|
|
309
|
+
Note: when the triggering exception was not a :class:`BrowserUseExecutionError`,
|
|
310
|
+
``details`` is empty and browser context cannot be recovered. In that case,
|
|
311
|
+
emit ``recording_status="unknown"`` to avoid misleadingly reporting
|
|
312
|
+
``"disabled"`` when a recording side effect may still have been started.
|
|
313
|
+
"""
|
|
314
|
+
browser_context: BrowserInfrastructureContext | None = None
|
|
315
|
+
streaming_url: str | None = None
|
|
316
|
+
|
|
317
|
+
browser_context_candidate = details.get("browser_context")
|
|
318
|
+
if isinstance(browser_context_candidate, BrowserInfrastructureContext):
|
|
319
|
+
browser_context = browser_context_candidate
|
|
320
|
+
|
|
321
|
+
streaming_url_candidate = details.get("streaming_url")
|
|
322
|
+
if streaming_url_candidate is not None:
|
|
323
|
+
streaming_url = coerce_string(streaming_url_candidate)
|
|
324
|
+
|
|
325
|
+
metadata = recording_metadata(
|
|
326
|
+
infrastructure=self.infrastructure,
|
|
327
|
+
storage=self.storage,
|
|
328
|
+
browser_context=browser_context,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
self.infrastructure is not None
|
|
333
|
+
and self.storage is not None
|
|
334
|
+
and browser_context is None
|
|
335
|
+
and details == {}
|
|
336
|
+
and metadata.get("recording_status") == "disabled"
|
|
337
|
+
and metadata.get("recording_message") == "Browser context is not available."
|
|
338
|
+
):
|
|
339
|
+
metadata.update(
|
|
340
|
+
{
|
|
341
|
+
"recording_status": "unknown",
|
|
342
|
+
"recording_message": "Recording status could not be determined from terminal error context.",
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return BrowserUseRunResult(
|
|
347
|
+
status="error",
|
|
348
|
+
task=task,
|
|
349
|
+
final_output="",
|
|
350
|
+
session_id=browser_context.session_id
|
|
351
|
+
if browser_context is not None
|
|
352
|
+
else None,
|
|
353
|
+
streaming_url=streaming_url,
|
|
354
|
+
recording_url=resolved_recording_url,
|
|
355
|
+
steps=0,
|
|
356
|
+
error=reason,
|
|
357
|
+
events=[],
|
|
358
|
+
metadata={
|
|
359
|
+
"attempt": attempt,
|
|
360
|
+
"max_attempts": max_attempts,
|
|
361
|
+
**metadata,
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def _resolve_recording_url_from_details(
|
|
366
|
+
self, details: dict[str, Any] | Any
|
|
367
|
+
) -> str | None:
|
|
368
|
+
if not isinstance(details, dict):
|
|
369
|
+
return None
|
|
370
|
+
candidate = details.get("browser_context")
|
|
371
|
+
if not isinstance(candidate, BrowserInfrastructureContext):
|
|
372
|
+
return None
|
|
373
|
+
if not self._can_record(candidate):
|
|
374
|
+
return None
|
|
375
|
+
assert self.infrastructure is not None
|
|
376
|
+
url = self.infrastructure.resolve_recording_url(
|
|
377
|
+
session_id=candidate.session_id, storage=self.storage
|
|
378
|
+
)
|
|
379
|
+
return coerce_string(url) if url is not None else None
|
|
380
|
+
|
|
381
|
+
async def _run_single_attempt(
|
|
382
|
+
self, task: str, emit: EmitCallable, *, attempt: int
|
|
383
|
+
) -> BrowserUseRunResult:
|
|
384
|
+
if not self.config.llm_openai_api_key:
|
|
385
|
+
raise BrowserUseConfigurationError(
|
|
386
|
+
"llm_openai_api_key is required for browser execution."
|
|
387
|
+
)
|
|
388
|
+
if not self.config.page_extraction_llm_openai_api_key:
|
|
389
|
+
raise BrowserUseConfigurationError(
|
|
390
|
+
"page_extraction_llm_openai_api_key is required for extraction."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if self.infrastructure is None:
|
|
394
|
+
logger.debug("Browser infrastructure is not configured.")
|
|
395
|
+
if self.storage is None:
|
|
396
|
+
logger.debug("Object storage is not configured.")
|
|
397
|
+
|
|
398
|
+
if self.storage is not None and not self.storage.is_available:
|
|
399
|
+
logger.debug(
|
|
400
|
+
"Object storage is not available: %s",
|
|
401
|
+
getattr(self.storage, "last_error", None),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# NOTE: browser-use reads these variables at module import / setup time.
|
|
405
|
+
# Writing them here is a process-global side effect; concurrent
|
|
406
|
+
# BrowserUseClient instances configured with different values will race.
|
|
407
|
+
os.environ["BROWSER_USE_CLOUD_SYNC"] = (
|
|
408
|
+
"true" if self.config.enable_cloud_sync else "false"
|
|
409
|
+
)
|
|
410
|
+
os.environ["BROWSER_USE_LOGGING_LEVEL"] = self.config.logging_level
|
|
411
|
+
with contextlib.suppress(Exception):
|
|
412
|
+
setup_logging(log_level=self.config.logging_level, force_setup=True)
|
|
413
|
+
|
|
414
|
+
browser_context = None
|
|
415
|
+
browser_session = None
|
|
416
|
+
agent = None
|
|
417
|
+
streaming_url = None
|
|
418
|
+
recording_url = None
|
|
419
|
+
step_count = 0
|
|
420
|
+
status: BrowserUseRunStatus = "error"
|
|
421
|
+
|
|
422
|
+
def _build_execution_error(
|
|
423
|
+
message: str, *, origin: BaseException | None = None
|
|
424
|
+
) -> BrowserUseExecutionError:
|
|
425
|
+
details = {
|
|
426
|
+
"browser_context": browser_context,
|
|
427
|
+
"streaming_url": streaming_url,
|
|
428
|
+
"recording_url": recording_url,
|
|
429
|
+
}
|
|
430
|
+
return BrowserUseExecutionError(message, details=details, origin=origin)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
if self.infrastructure is not None:
|
|
434
|
+
browser_context = await self.infrastructure.create_session()
|
|
435
|
+
streaming_url = coerce_string(browser_context.streaming_url)
|
|
436
|
+
if streaming_url:
|
|
437
|
+
await _maybe_await(
|
|
438
|
+
emit(build_activity_event(streaming_url, STREAMING_MESSAGE))
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
cdp_url = self.infrastructure.cdp_url(
|
|
442
|
+
session_id=browser_context.session_id
|
|
443
|
+
)
|
|
444
|
+
browser_session = BrowserSession(cdp_url=cdp_url)
|
|
445
|
+
# TODO(upstream: browser_use): `_owns_browser_resources` is a private
|
|
446
|
+
# attribute of BrowserSession; if it is renamed in 0.12+ this flag
|
|
447
|
+
# will silently become a no-op.
|
|
448
|
+
with contextlib.suppress(Exception):
|
|
449
|
+
setattr(browser_session, "_owns_browser_resources", True)
|
|
450
|
+
|
|
451
|
+
llm = self._build_llm(
|
|
452
|
+
model=self.config.llm_openai_model,
|
|
453
|
+
reasoning_effort=self.config.llm_openai_reasoning_effort,
|
|
454
|
+
temperature=self.config.llm_openai_temperature,
|
|
455
|
+
api_key=self.config.llm_openai_api_key,
|
|
456
|
+
base_url=self.config.llm_openai_base_url,
|
|
457
|
+
)
|
|
458
|
+
page_extraction_llm = self._build_llm(
|
|
459
|
+
model=self.config.page_extraction_llm_openai_model,
|
|
460
|
+
reasoning_effort=self.config.page_extraction_llm_openai_reasoning_effort,
|
|
461
|
+
temperature=self.config.page_extraction_llm_openai_temperature,
|
|
462
|
+
api_key=self.config.page_extraction_llm_openai_api_key,
|
|
463
|
+
base_url=self.config.page_extraction_llm_openai_base_url,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
agent = Agent(
|
|
467
|
+
task=task,
|
|
468
|
+
llm=llm,
|
|
469
|
+
page_extraction_llm=page_extraction_llm,
|
|
470
|
+
browser_session=browser_session,
|
|
471
|
+
extend_system_message=self.config.extend_system_message,
|
|
472
|
+
vision_detail_level=self.config.vision_detail_level,
|
|
473
|
+
llm_timeout=self.config.llm_timeout_in_s,
|
|
474
|
+
step_timeout=self.config.step_timeout_in_s,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if browser_session is not None:
|
|
478
|
+
with contextlib.suppress(Exception):
|
|
479
|
+
await browser_session.start()
|
|
480
|
+
|
|
481
|
+
while True:
|
|
482
|
+
try:
|
|
483
|
+
is_done, _ = await agent.take_step()
|
|
484
|
+
step_count += 1
|
|
485
|
+
except Exception as exc:
|
|
486
|
+
raise _build_execution_error(
|
|
487
|
+
f"Error in step execution: {exc}", origin=exc
|
|
488
|
+
) from exc
|
|
489
|
+
|
|
490
|
+
tool_calls = extract_tool_calls(agent)
|
|
491
|
+
extraction_error = validate_structured_data_output(tool_calls)
|
|
492
|
+
if extraction_error:
|
|
493
|
+
raise _build_execution_error(extraction_error)
|
|
494
|
+
|
|
495
|
+
step_content = build_step_content(tool_calls, is_done=is_done)
|
|
496
|
+
step_event = build_step_event(
|
|
497
|
+
tool_calls, step_content, is_final=is_done
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if is_done:
|
|
501
|
+
await _maybe_await(emit(step_event))
|
|
502
|
+
status = "success"
|
|
503
|
+
break
|
|
504
|
+
|
|
505
|
+
await _maybe_await(
|
|
506
|
+
emit(
|
|
507
|
+
build_thinking_event("thinking_start", content="Thinking start")
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
await _maybe_await(emit(step_event))
|
|
511
|
+
await _maybe_await(
|
|
512
|
+
emit(build_thinking_event("thinking_end", content="Thinking end"))
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
final_output = extract_final_output(agent)
|
|
516
|
+
if not final_output:
|
|
517
|
+
final_output = TASK_COMPLETED_MESSAGE
|
|
518
|
+
|
|
519
|
+
if self._can_record(browser_context):
|
|
520
|
+
assert self.infrastructure is not None
|
|
521
|
+
recording_url = self.infrastructure.resolve_recording_url(
|
|
522
|
+
session_id=browser_context.session_id, storage=self.storage
|
|
523
|
+
)
|
|
524
|
+
if recording_url:
|
|
525
|
+
await _maybe_await(
|
|
526
|
+
emit(build_activity_event(recording_url, RECORDING_MESSAGE))
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
metadata = recording_metadata(
|
|
530
|
+
infrastructure=self.infrastructure,
|
|
531
|
+
storage=self.storage,
|
|
532
|
+
browser_context=browser_context,
|
|
533
|
+
)
|
|
534
|
+
return BrowserUseRunResult(
|
|
535
|
+
status=status,
|
|
536
|
+
task=task,
|
|
537
|
+
final_output=final_output,
|
|
538
|
+
session_id=browser_context.session_id
|
|
539
|
+
if browser_context is not None
|
|
540
|
+
else None,
|
|
541
|
+
streaming_url=streaming_url,
|
|
542
|
+
recording_url=recording_url,
|
|
543
|
+
steps=step_count,
|
|
544
|
+
error=None,
|
|
545
|
+
events=[],
|
|
546
|
+
metadata={
|
|
547
|
+
"attempt": attempt,
|
|
548
|
+
**metadata,
|
|
549
|
+
},
|
|
550
|
+
)
|
|
551
|
+
finally:
|
|
552
|
+
if self._can_record(browser_context):
|
|
553
|
+
assert self.infrastructure is not None
|
|
554
|
+
self.infrastructure.schedule_recording(
|
|
555
|
+
browser_context, storage=self.storage
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if agent is not None:
|
|
559
|
+
agent_eventbus: EventBus | None = getattr(agent, "eventbus", None)
|
|
560
|
+
if agent_eventbus is not None:
|
|
561
|
+
try:
|
|
562
|
+
await agent_eventbus.stop(
|
|
563
|
+
clear=True, timeout=_AGENT_EVENTBUS_STOP_TIMEOUT_IN_S
|
|
564
|
+
)
|
|
565
|
+
except Exception as exc:
|
|
566
|
+
logger.debug(
|
|
567
|
+
"Failed to stop agent event bus during cleanup: %s",
|
|
568
|
+
exc,
|
|
569
|
+
exc_info=True,
|
|
570
|
+
)
|
|
571
|
+
try:
|
|
572
|
+
await agent.close()
|
|
573
|
+
except Exception as exc:
|
|
574
|
+
logger.debug(
|
|
575
|
+
"Failed to close agent during cleanup: %s", exc, exc_info=True
|
|
576
|
+
)
|
|
577
|
+
elif browser_session is not None:
|
|
578
|
+
try:
|
|
579
|
+
await browser_session.kill()
|
|
580
|
+
except Exception as exc:
|
|
581
|
+
logger.debug(
|
|
582
|
+
"Failed to stop browser session during cleanup: %s",
|
|
583
|
+
exc,
|
|
584
|
+
exc_info=True,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if self.infrastructure is not None and browser_context is not None:
|
|
588
|
+
try:
|
|
589
|
+
await self.infrastructure.release_session(browser_context)
|
|
590
|
+
except Exception as exc:
|
|
591
|
+
logger.debug(
|
|
592
|
+
"Failed to release browser infrastructure session %s during cleanup: %s",
|
|
593
|
+
browser_context.session_id,
|
|
594
|
+
exc,
|
|
595
|
+
exc_info=True,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _build_llm(
|
|
599
|
+
self,
|
|
600
|
+
*,
|
|
601
|
+
model: ChatModel | str,
|
|
602
|
+
reasoning_effort: ReasoningEffort,
|
|
603
|
+
temperature: float | None,
|
|
604
|
+
api_key: str,
|
|
605
|
+
base_url: str | None,
|
|
606
|
+
) -> ChatOpenAI:
|
|
607
|
+
llm_kwargs: dict[str, Any] = {
|
|
608
|
+
"model": model,
|
|
609
|
+
"reasoning_effort": reasoning_effort,
|
|
610
|
+
"api_key": api_key,
|
|
611
|
+
"base_url": base_url,
|
|
612
|
+
}
|
|
613
|
+
if self._supports_tunable_params(model):
|
|
614
|
+
llm_kwargs["temperature"] = temperature
|
|
615
|
+
else:
|
|
616
|
+
llm_kwargs["temperature"] = None
|
|
617
|
+
llm_kwargs["frequency_penalty"] = None
|
|
618
|
+
return ChatOpenAI(**llm_kwargs)
|
|
619
|
+
|
|
620
|
+
def _supports_tunable_params(self, model: ChatModel | str) -> bool:
|
|
621
|
+
model_name = coerce_string(model).lower()
|
|
622
|
+
if model_name.startswith("gpt-5"):
|
|
623
|
+
return False
|
|
624
|
+
short_name = model_name.split("-", 1)[0]
|
|
625
|
+
return not (short_name.startswith("o") and short_name[1:].isdigit())
|
|
626
|
+
|
|
627
|
+
def _can_record(self, browser_context: BrowserInfrastructureContext | None) -> bool:
|
|
628
|
+
return self.infrastructure is not None and self.infrastructure.can_record(
|
|
629
|
+
browser_context, self.storage
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
@staticmethod
|
|
633
|
+
def _validate_task(task: str) -> str:
|
|
634
|
+
if not isinstance(task, str):
|
|
635
|
+
raise BrowserUseExecutionError("task must be a non-empty string")
|
|
636
|
+
value = task.strip()
|
|
637
|
+
if not value:
|
|
638
|
+
raise BrowserUseExecutionError("task must be a non-empty string")
|
|
639
|
+
return value
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
3
|
+
from gl_browser_use.config import BrowserUseClientConfig as BrowserUseClientConfig
|
|
4
|
+
from gl_browser_use.errors import BrowserUseConfigurationError as BrowserUseConfigurationError, BrowserUseExecutionError as BrowserUseExecutionError, BrowserUseRetryExhaustedError as BrowserUseRetryExhaustedError
|
|
5
|
+
from gl_browser_use.events import RECORDING_MESSAGE as RECORDING_MESSAGE, STREAMING_MESSAGE as STREAMING_MESSAGE, TASK_COMPLETED_MESSAGE as TASK_COMPLETED_MESSAGE, build_activity_event as build_activity_event, build_error_event as build_error_event, build_retry_event as build_retry_event, build_step_content as build_step_content, build_step_event as build_step_event, build_thinking_event as build_thinking_event
|
|
6
|
+
from gl_browser_use.infrastructure.base import BrowserInfrastructure as BrowserInfrastructure, BrowserInfrastructureContext as BrowserInfrastructureContext
|
|
7
|
+
from gl_browser_use.models import BrowserUseRunResult as BrowserUseRunResult, BrowserUseRunStatus as BrowserUseRunStatus, BrowserUseStreamEvent as BrowserUseStreamEvent
|
|
8
|
+
from gl_browser_use.parsing import coerce_string as coerce_string, extract_final_output as extract_final_output, extract_tool_calls as extract_tool_calls, summarize_terminal_error as summarize_terminal_error, validate_structured_data_output as validate_structured_data_output
|
|
9
|
+
from gl_browser_use.session_errors import is_recoverable_session_error as is_recoverable_session_error
|
|
10
|
+
from gl_browser_use.storage.base import ObjectStorage as ObjectStorage
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
logger: Incomplete
|
|
14
|
+
EmitCallable = Callable[[BrowserUseStreamEvent | None], Awaitable[None] | None]
|
|
15
|
+
|
|
16
|
+
def recording_metadata(*, infrastructure: BrowserInfrastructure | None, storage: ObjectStorage | None, browser_context: BrowserInfrastructureContext | None) -> dict[str, Any]:
|
|
17
|
+
"""Return recording status metadata aligned with implementation behavior."""
|
|
18
|
+
|
|
19
|
+
class BrowserUseClient:
|
|
20
|
+
"""Facade for running browser automation tasks.
|
|
21
|
+
|
|
22
|
+
Note: ``infrastructure`` and ``storage`` attributes are read by ``run``/
|
|
23
|
+
``run_once``/``run_sync`` on each call. Swapping them while a run is in
|
|
24
|
+
flight is undefined behavior.
|
|
25
|
+
"""
|
|
26
|
+
config: Incomplete
|
|
27
|
+
infrastructure: Incomplete
|
|
28
|
+
storage: Incomplete
|
|
29
|
+
def __init__(self, *, config: BrowserUseClientConfig, infrastructure: BrowserInfrastructure | None = None, storage: ObjectStorage | None = None) -> None:
|
|
30
|
+
"""Initialize the client."""
|
|
31
|
+
async def run(self, task: str) -> AsyncGenerator[BrowserUseStreamEvent, None]:
|
|
32
|
+
"""Run the client."""
|
|
33
|
+
async def run_once(self, task: str) -> BrowserUseRunResult:
|
|
34
|
+
"""Run the client once and return a single aggregated result.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
------
|
|
38
|
+
BrowserUseExecutionError
|
|
39
|
+
When the task input is invalid.
|
|
40
|
+
BrowserUseConfigurationError
|
|
41
|
+
When required configuration is missing.
|
|
42
|
+
BrowserUseRetryExhaustedError
|
|
43
|
+
When all recoverable session retries are exhausted.
|
|
44
|
+
"""
|
|
45
|
+
def run_sync(self, task: str) -> BrowserUseRunResult:
|
|
46
|
+
"""Run the client synchronously.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
------
|
|
50
|
+
BrowserUseExecutionError
|
|
51
|
+
When invoked from a thread that already has a running event loop,
|
|
52
|
+
or when task validation fails.
|
|
53
|
+
BrowserUseConfigurationError
|
|
54
|
+
When required configuration is missing.
|
|
55
|
+
BrowserUseRetryExhaustedError
|
|
56
|
+
When all recoverable session retries are exhausted.
|
|
57
|
+
"""
|