optexity-browser-use 0.9.5__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.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatBrowserUse - Client for browser-use cloud API
|
|
3
|
+
|
|
4
|
+
This wraps the BaseChatModel protocol and sends requests to the browser-use cloud API
|
|
5
|
+
for optimized browser automation LLM inference.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import TypeVar, overload
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from browser_use.llm.base import BaseChatModel
|
|
16
|
+
from browser_use.llm.messages import BaseMessage
|
|
17
|
+
from browser_use.llm.views import ChatInvokeCompletion
|
|
18
|
+
from browser_use.observability import observe
|
|
19
|
+
|
|
20
|
+
T = TypeVar('T', bound=BaseModel)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChatBrowserUse(BaseChatModel):
|
|
26
|
+
"""
|
|
27
|
+
Client for browser-use cloud API.
|
|
28
|
+
|
|
29
|
+
This sends requests to the browser-use cloud API which uses optimized models
|
|
30
|
+
and prompts for browser automation tasks.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
agent = Agent(
|
|
34
|
+
task="Find the number of stars of the browser-use repo",
|
|
35
|
+
llm=ChatBrowserUse(model='bu-latest'),
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
model: str = 'bu-latest',
|
|
42
|
+
api_key: str | None = None,
|
|
43
|
+
base_url: str | None = None,
|
|
44
|
+
timeout: float = 120.0,
|
|
45
|
+
**kwargs,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize ChatBrowserUse client.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
model: Model name to use. Options: 'bu-latest', 'bu-1-0'. Defaults to 'bu-latest'.
|
|
52
|
+
api_key: API key for browser-use cloud. Defaults to BROWSER_USE_API_KEY env var.
|
|
53
|
+
base_url: Base URL for the API. Defaults to BROWSER_USE_LLM_URL env var or production URL.
|
|
54
|
+
timeout: Request timeout in seconds.
|
|
55
|
+
"""
|
|
56
|
+
# Validate model name
|
|
57
|
+
valid_models = ['bu-latest', 'bu-1-0']
|
|
58
|
+
if model not in valid_models:
|
|
59
|
+
raise ValueError(f"Invalid model: '{model}'. Must be one of {valid_models}")
|
|
60
|
+
|
|
61
|
+
self.model = 'bu-1-0' if model == 'bu-latest' else model # must update on new model releases
|
|
62
|
+
self.fast = False
|
|
63
|
+
self.api_key = api_key or os.getenv('BROWSER_USE_API_KEY')
|
|
64
|
+
self.base_url = base_url or os.getenv('BROWSER_USE_LLM_URL', 'https://llm.api.browser-use.com')
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
|
|
67
|
+
if not self.api_key:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
'You need to set the BROWSER_USE_API_KEY environment variable. '
|
|
70
|
+
'Get your key at https://cloud.browser-use.com/new-api-key'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def provider(self) -> str:
|
|
75
|
+
return 'browser-use'
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def name(self) -> str:
|
|
79
|
+
return self.model
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
async def ainvoke(
|
|
83
|
+
self, messages: list[BaseMessage], output_format: None = None, request_type: str = 'browser_agent'
|
|
84
|
+
) -> ChatInvokeCompletion[str]: ...
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
async def ainvoke(
|
|
88
|
+
self, messages: list[BaseMessage], output_format: type[T], request_type: str = 'browser_agent'
|
|
89
|
+
) -> ChatInvokeCompletion[T]: ...
|
|
90
|
+
|
|
91
|
+
@observe(name='chat_browser_use_ainvoke')
|
|
92
|
+
async def ainvoke(
|
|
93
|
+
self, messages: list[BaseMessage], output_format: type[T] | None = None, request_type: str = 'browser_agent'
|
|
94
|
+
) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
|
|
95
|
+
"""
|
|
96
|
+
Send request to browser-use cloud API.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
messages: List of messages to send
|
|
100
|
+
output_format: Expected output format (Pydantic model)
|
|
101
|
+
request_type: Type of request - 'browser_agent' or 'judge'
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
ChatInvokeCompletion with structured response and usage info
|
|
105
|
+
"""
|
|
106
|
+
# Prepare request payload
|
|
107
|
+
payload = {
|
|
108
|
+
'messages': [self._serialize_message(msg) for msg in messages],
|
|
109
|
+
'fast': self.fast,
|
|
110
|
+
'request_type': request_type,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Add output format schema if provided
|
|
114
|
+
if output_format is not None:
|
|
115
|
+
payload['output_format'] = output_format.model_json_schema()
|
|
116
|
+
|
|
117
|
+
# Make API request
|
|
118
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
119
|
+
try:
|
|
120
|
+
response = await client.post(
|
|
121
|
+
f'{self.base_url}/v1/chat/completions',
|
|
122
|
+
json=payload,
|
|
123
|
+
headers={
|
|
124
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
result = response.json()
|
|
130
|
+
|
|
131
|
+
except httpx.HTTPStatusError as e:
|
|
132
|
+
error_detail = ''
|
|
133
|
+
try:
|
|
134
|
+
error_data = e.response.json()
|
|
135
|
+
error_detail = error_data.get('detail', str(e))
|
|
136
|
+
except Exception:
|
|
137
|
+
error_detail = str(e)
|
|
138
|
+
|
|
139
|
+
error_msg = ''
|
|
140
|
+
if e.response.status_code == 401:
|
|
141
|
+
error_msg = f'Invalid API key. {error_detail}'
|
|
142
|
+
elif e.response.status_code == 402:
|
|
143
|
+
error_msg = f'Insufficient credits. {error_detail}'
|
|
144
|
+
else:
|
|
145
|
+
error_msg = f'API request failed: {error_detail}'
|
|
146
|
+
|
|
147
|
+
raise ValueError(error_msg)
|
|
148
|
+
|
|
149
|
+
except httpx.TimeoutException:
|
|
150
|
+
error_msg = f'Request timed out after {self.timeout}s'
|
|
151
|
+
raise ValueError(error_msg)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
error_msg = f'Failed to connect to browser-use API: {e}'
|
|
155
|
+
raise ValueError(error_msg)
|
|
156
|
+
|
|
157
|
+
# Parse response - server returns structured data as dict
|
|
158
|
+
if output_format is not None:
|
|
159
|
+
# Server returns structured data as a dict, validate it
|
|
160
|
+
completion_data = result['completion']
|
|
161
|
+
logger.debug(
|
|
162
|
+
f'📥 Got structured data from service: {list(completion_data.keys()) if isinstance(completion_data, dict) else type(completion_data)}'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Convert action dicts to ActionModel instances if needed
|
|
166
|
+
# llm-use returns dicts to avoid validation with empty ActionModel
|
|
167
|
+
if isinstance(completion_data, dict) and 'action' in completion_data:
|
|
168
|
+
actions = completion_data['action']
|
|
169
|
+
if actions and isinstance(actions[0], dict):
|
|
170
|
+
from typing import get_args
|
|
171
|
+
|
|
172
|
+
# Get ActionModel type from output_format
|
|
173
|
+
action_model_type = get_args(output_format.model_fields['action'].annotation)[0]
|
|
174
|
+
|
|
175
|
+
# Convert dicts to ActionModel instances
|
|
176
|
+
completion_data['action'] = [action_model_type.model_validate(action_dict) for action_dict in actions]
|
|
177
|
+
|
|
178
|
+
completion = output_format.model_validate(completion_data)
|
|
179
|
+
else:
|
|
180
|
+
completion = result['completion']
|
|
181
|
+
|
|
182
|
+
# Parse usage info
|
|
183
|
+
usage = None
|
|
184
|
+
if 'usage' in result:
|
|
185
|
+
from browser_use.llm.views import ChatInvokeUsage
|
|
186
|
+
|
|
187
|
+
usage = ChatInvokeUsage(**result['usage'])
|
|
188
|
+
|
|
189
|
+
return ChatInvokeCompletion(
|
|
190
|
+
completion=completion,
|
|
191
|
+
usage=usage,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _serialize_message(self, message: BaseMessage) -> dict:
|
|
195
|
+
"""Serialize a message to JSON format."""
|
|
196
|
+
# Handle Union types by checking the actual message type
|
|
197
|
+
msg_dict = message.model_dump()
|
|
198
|
+
return {
|
|
199
|
+
'role': msg_dict['role'],
|
|
200
|
+
'content': msg_dict['content'],
|
|
201
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, TypeVar, overload
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from openai import (
|
|
8
|
+
APIConnectionError,
|
|
9
|
+
APIError,
|
|
10
|
+
APIStatusError,
|
|
11
|
+
APITimeoutError,
|
|
12
|
+
AsyncOpenAI,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
)
|
|
15
|
+
from openai.types.chat import ChatCompletion
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from browser_use.llm.base import BaseChatModel
|
|
19
|
+
from browser_use.llm.cerebras.serializer import CerebrasMessageSerializer
|
|
20
|
+
from browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError
|
|
21
|
+
from browser_use.llm.messages import BaseMessage
|
|
22
|
+
from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage
|
|
23
|
+
|
|
24
|
+
T = TypeVar('T', bound=BaseModel)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ChatCerebras(BaseChatModel):
|
|
29
|
+
"""Cerebras inference wrapper (OpenAI-compatible)."""
|
|
30
|
+
|
|
31
|
+
model: str = 'llama3.1-8b'
|
|
32
|
+
|
|
33
|
+
# Generation parameters
|
|
34
|
+
max_tokens: int | None = 4096
|
|
35
|
+
temperature: float | None = 0.2
|
|
36
|
+
top_p: float | None = None
|
|
37
|
+
seed: int | None = None
|
|
38
|
+
|
|
39
|
+
# Connection parameters
|
|
40
|
+
api_key: str | None = None
|
|
41
|
+
base_url: str | httpx.URL | None = 'https://api.cerebras.ai/v1'
|
|
42
|
+
timeout: float | httpx.Timeout | None = None
|
|
43
|
+
client_params: dict[str, Any] | None = None
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def provider(self) -> str:
|
|
47
|
+
return 'cerebras'
|
|
48
|
+
|
|
49
|
+
def _client(self) -> AsyncOpenAI:
|
|
50
|
+
return AsyncOpenAI(
|
|
51
|
+
api_key=self.api_key,
|
|
52
|
+
base_url=self.base_url,
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
**(self.client_params or {}),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def name(self) -> str:
|
|
59
|
+
return self.model
|
|
60
|
+
|
|
61
|
+
def _get_usage(self, response: ChatCompletion) -> ChatInvokeUsage | None:
|
|
62
|
+
if response.usage is not None:
|
|
63
|
+
usage = ChatInvokeUsage(
|
|
64
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
65
|
+
prompt_cached_tokens=None,
|
|
66
|
+
prompt_cache_creation_tokens=None,
|
|
67
|
+
prompt_image_tokens=None,
|
|
68
|
+
completion_tokens=response.usage.completion_tokens,
|
|
69
|
+
total_tokens=response.usage.total_tokens,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
usage = None
|
|
73
|
+
return usage
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
async def ainvoke(
|
|
77
|
+
self,
|
|
78
|
+
messages: list[BaseMessage],
|
|
79
|
+
output_format: None = None,
|
|
80
|
+
) -> ChatInvokeCompletion[str]: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
async def ainvoke(
|
|
84
|
+
self,
|
|
85
|
+
messages: list[BaseMessage],
|
|
86
|
+
output_format: type[T],
|
|
87
|
+
) -> ChatInvokeCompletion[T]: ...
|
|
88
|
+
|
|
89
|
+
async def ainvoke(
|
|
90
|
+
self,
|
|
91
|
+
messages: list[BaseMessage],
|
|
92
|
+
output_format: type[T] | None = None,
|
|
93
|
+
) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
|
|
94
|
+
"""
|
|
95
|
+
Cerebras ainvoke supports:
|
|
96
|
+
1. Regular text/multi-turn conversation
|
|
97
|
+
2. JSON Output (response_format)
|
|
98
|
+
"""
|
|
99
|
+
client = self._client()
|
|
100
|
+
cerebras_messages = CerebrasMessageSerializer.serialize_messages(messages)
|
|
101
|
+
common: dict[str, Any] = {}
|
|
102
|
+
|
|
103
|
+
if self.temperature is not None:
|
|
104
|
+
common['temperature'] = self.temperature
|
|
105
|
+
if self.max_tokens is not None:
|
|
106
|
+
common['max_tokens'] = self.max_tokens
|
|
107
|
+
if self.top_p is not None:
|
|
108
|
+
common['top_p'] = self.top_p
|
|
109
|
+
if self.seed is not None:
|
|
110
|
+
common['seed'] = self.seed
|
|
111
|
+
|
|
112
|
+
# ① Regular multi-turn conversation/text output
|
|
113
|
+
if output_format is None:
|
|
114
|
+
try:
|
|
115
|
+
resp = await client.chat.completions.create( # type: ignore
|
|
116
|
+
model=self.model,
|
|
117
|
+
messages=cerebras_messages, # type: ignore
|
|
118
|
+
**common,
|
|
119
|
+
)
|
|
120
|
+
usage = self._get_usage(resp)
|
|
121
|
+
return ChatInvokeCompletion(
|
|
122
|
+
completion=resp.choices[0].message.content or '',
|
|
123
|
+
usage=usage,
|
|
124
|
+
)
|
|
125
|
+
except RateLimitError as e:
|
|
126
|
+
raise ModelRateLimitError(str(e), model=self.name) from e
|
|
127
|
+
except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:
|
|
128
|
+
raise ModelProviderError(str(e), model=self.name) from e
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise ModelProviderError(str(e), model=self.name) from e
|
|
131
|
+
|
|
132
|
+
# ② JSON Output path (response_format)
|
|
133
|
+
if output_format is not None and hasattr(output_format, 'model_json_schema'):
|
|
134
|
+
try:
|
|
135
|
+
# For Cerebras, we'll use a simpler approach without response_format
|
|
136
|
+
# Instead, we'll ask the model to return JSON and parse it
|
|
137
|
+
import json
|
|
138
|
+
|
|
139
|
+
# Get the schema to guide the model
|
|
140
|
+
schema = output_format.model_json_schema()
|
|
141
|
+
schema_str = json.dumps(schema, indent=2)
|
|
142
|
+
|
|
143
|
+
# Create a prompt that asks for the specific JSON structure
|
|
144
|
+
json_prompt = f"""
|
|
145
|
+
Please respond with a JSON object that follows this exact schema:
|
|
146
|
+
{schema_str}
|
|
147
|
+
|
|
148
|
+
Your response must be valid JSON only, no other text.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Add or modify the last user message to include the JSON prompt
|
|
152
|
+
if cerebras_messages and cerebras_messages[-1]['role'] == 'user':
|
|
153
|
+
if isinstance(cerebras_messages[-1]['content'], str):
|
|
154
|
+
cerebras_messages[-1]['content'] += json_prompt
|
|
155
|
+
elif isinstance(cerebras_messages[-1]['content'], list):
|
|
156
|
+
cerebras_messages[-1]['content'].append({'type': 'text', 'text': json_prompt})
|
|
157
|
+
else:
|
|
158
|
+
# Add as a new user message
|
|
159
|
+
cerebras_messages.append({'role': 'user', 'content': json_prompt})
|
|
160
|
+
|
|
161
|
+
resp = await client.chat.completions.create( # type: ignore
|
|
162
|
+
model=self.model,
|
|
163
|
+
messages=cerebras_messages, # type: ignore
|
|
164
|
+
**common,
|
|
165
|
+
)
|
|
166
|
+
content = resp.choices[0].message.content
|
|
167
|
+
if not content:
|
|
168
|
+
raise ModelProviderError('Empty JSON content in Cerebras response', model=self.name)
|
|
169
|
+
|
|
170
|
+
usage = self._get_usage(resp)
|
|
171
|
+
|
|
172
|
+
# Try to extract JSON from the response
|
|
173
|
+
import re
|
|
174
|
+
|
|
175
|
+
json_match = re.search(r'\{.*\}', content, re.DOTALL)
|
|
176
|
+
if json_match:
|
|
177
|
+
json_str = json_match.group(0)
|
|
178
|
+
else:
|
|
179
|
+
json_str = content
|
|
180
|
+
|
|
181
|
+
parsed = output_format.model_validate_json(json_str)
|
|
182
|
+
return ChatInvokeCompletion(
|
|
183
|
+
completion=parsed,
|
|
184
|
+
usage=usage,
|
|
185
|
+
)
|
|
186
|
+
except RateLimitError as e:
|
|
187
|
+
raise ModelRateLimitError(str(e), model=self.name) from e
|
|
188
|
+
except (APIError, APIConnectionError, APITimeoutError, APIStatusError) as e:
|
|
189
|
+
raise ModelProviderError(str(e), model=self.name) from e
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise ModelProviderError(str(e), model=self.name) from e
|
|
192
|
+
|
|
193
|
+
raise ModelProviderError('No valid ainvoke execution path for Cerebras LLM', model=self.name)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, overload
|
|
5
|
+
|
|
6
|
+
from browser_use.llm.messages import (
|
|
7
|
+
AssistantMessage,
|
|
8
|
+
BaseMessage,
|
|
9
|
+
ContentPartImageParam,
|
|
10
|
+
ContentPartTextParam,
|
|
11
|
+
SystemMessage,
|
|
12
|
+
ToolCall,
|
|
13
|
+
UserMessage,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
MessageDict = dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CerebrasMessageSerializer:
|
|
20
|
+
"""Serializer for converting browser-use messages to Cerebras messages."""
|
|
21
|
+
|
|
22
|
+
# -------- content 处理 --------------------------------------------------
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _serialize_text_part(part: ContentPartTextParam) -> str:
|
|
25
|
+
return part.text
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _serialize_image_part(part: ContentPartImageParam) -> dict[str, Any]:
|
|
29
|
+
url = part.image_url.url
|
|
30
|
+
if url.startswith('data:'):
|
|
31
|
+
return {'type': 'image_url', 'image_url': {'url': url}}
|
|
32
|
+
return {'type': 'image_url', 'image_url': {'url': url}}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _serialize_content(content: Any) -> str | list[dict[str, Any]]:
|
|
36
|
+
if content is None:
|
|
37
|
+
return ''
|
|
38
|
+
if isinstance(content, str):
|
|
39
|
+
return content
|
|
40
|
+
serialized: list[dict[str, Any]] = []
|
|
41
|
+
for part in content:
|
|
42
|
+
if part.type == 'text':
|
|
43
|
+
serialized.append({'type': 'text', 'text': CerebrasMessageSerializer._serialize_text_part(part)})
|
|
44
|
+
elif part.type == 'image_url':
|
|
45
|
+
serialized.append(CerebrasMessageSerializer._serialize_image_part(part))
|
|
46
|
+
elif part.type == 'refusal':
|
|
47
|
+
serialized.append({'type': 'text', 'text': f'[Refusal] {part.refusal}'})
|
|
48
|
+
return serialized
|
|
49
|
+
|
|
50
|
+
# -------- Tool-call 处理 -------------------------------------------------
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _serialize_tool_calls(tool_calls: list[ToolCall]) -> list[dict[str, Any]]:
|
|
53
|
+
cerebras_tool_calls: list[dict[str, Any]] = []
|
|
54
|
+
for tc in tool_calls:
|
|
55
|
+
try:
|
|
56
|
+
arguments = json.loads(tc.function.arguments)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
arguments = {'arguments': tc.function.arguments}
|
|
59
|
+
cerebras_tool_calls.append(
|
|
60
|
+
{
|
|
61
|
+
'id': tc.id,
|
|
62
|
+
'type': 'function',
|
|
63
|
+
'function': {
|
|
64
|
+
'name': tc.function.name,
|
|
65
|
+
'arguments': arguments,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
return cerebras_tool_calls
|
|
70
|
+
|
|
71
|
+
# -------- 单条消息序列化 -------------------------------------------------
|
|
72
|
+
@overload
|
|
73
|
+
@staticmethod
|
|
74
|
+
def serialize(message: UserMessage) -> MessageDict: ...
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
@staticmethod
|
|
78
|
+
def serialize(message: SystemMessage) -> MessageDict: ...
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
@staticmethod
|
|
82
|
+
def serialize(message: AssistantMessage) -> MessageDict: ...
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def serialize(message: BaseMessage) -> MessageDict:
|
|
86
|
+
if isinstance(message, UserMessage):
|
|
87
|
+
return {
|
|
88
|
+
'role': 'user',
|
|
89
|
+
'content': CerebrasMessageSerializer._serialize_content(message.content),
|
|
90
|
+
}
|
|
91
|
+
if isinstance(message, SystemMessage):
|
|
92
|
+
return {
|
|
93
|
+
'role': 'system',
|
|
94
|
+
'content': CerebrasMessageSerializer._serialize_content(message.content),
|
|
95
|
+
}
|
|
96
|
+
if isinstance(message, AssistantMessage):
|
|
97
|
+
msg: MessageDict = {
|
|
98
|
+
'role': 'assistant',
|
|
99
|
+
'content': CerebrasMessageSerializer._serialize_content(message.content),
|
|
100
|
+
}
|
|
101
|
+
if message.tool_calls:
|
|
102
|
+
msg['tool_calls'] = CerebrasMessageSerializer._serialize_tool_calls(message.tool_calls)
|
|
103
|
+
return msg
|
|
104
|
+
raise ValueError(f'Unknown message type: {type(message)}')
|
|
105
|
+
|
|
106
|
+
# -------- 列表序列化 -----------------------------------------------------
|
|
107
|
+
@staticmethod
|
|
108
|
+
def serialize_messages(messages: list[BaseMessage]) -> list[MessageDict]:
|
|
109
|
+
return [CerebrasMessageSerializer.serialize(m) for m in messages]
|