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,229 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Literal, TypeVar, overload
|
|
4
|
+
|
|
5
|
+
from groq import (
|
|
6
|
+
APIError,
|
|
7
|
+
APIResponseValidationError,
|
|
8
|
+
APIStatusError,
|
|
9
|
+
AsyncGroq,
|
|
10
|
+
NotGiven,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
Timeout,
|
|
13
|
+
)
|
|
14
|
+
from groq.types.chat import ChatCompletion, ChatCompletionToolChoiceOptionParam, ChatCompletionToolParam
|
|
15
|
+
from groq.types.chat.completion_create_params import (
|
|
16
|
+
ResponseFormatResponseFormatJsonSchema,
|
|
17
|
+
ResponseFormatResponseFormatJsonSchemaJsonSchema,
|
|
18
|
+
)
|
|
19
|
+
from httpx import URL
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from browser_use.llm.base import BaseChatModel, ChatInvokeCompletion
|
|
23
|
+
from browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError
|
|
24
|
+
from browser_use.llm.groq.parser import try_parse_groq_failed_generation
|
|
25
|
+
from browser_use.llm.groq.serializer import GroqMessageSerializer
|
|
26
|
+
from browser_use.llm.messages import BaseMessage
|
|
27
|
+
from browser_use.llm.schema import SchemaOptimizer
|
|
28
|
+
from browser_use.llm.views import ChatInvokeUsage
|
|
29
|
+
|
|
30
|
+
GroqVerifiedModels = Literal[
|
|
31
|
+
'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
32
|
+
'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
33
|
+
'qwen/qwen3-32b',
|
|
34
|
+
'moonshotai/kimi-k2-instruct',
|
|
35
|
+
'openai/gpt-oss-20b',
|
|
36
|
+
'openai/gpt-oss-120b',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
JsonSchemaModels = [
|
|
40
|
+
'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
41
|
+
'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
42
|
+
'openai/gpt-oss-20b',
|
|
43
|
+
'openai/gpt-oss-120b',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ToolCallingModels = [
|
|
47
|
+
'moonshotai/kimi-k2-instruct',
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
T = TypeVar('T', bound=BaseModel)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ChatGroq(BaseChatModel):
|
|
57
|
+
"""
|
|
58
|
+
A wrapper around AsyncGroq that implements the BaseLLM protocol.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Model configuration
|
|
62
|
+
model: GroqVerifiedModels | str
|
|
63
|
+
|
|
64
|
+
# Model params
|
|
65
|
+
temperature: float | None = None
|
|
66
|
+
service_tier: Literal['auto', 'on_demand', 'flex'] | None = None
|
|
67
|
+
top_p: float | None = None
|
|
68
|
+
seed: int | None = None
|
|
69
|
+
|
|
70
|
+
# Client initialization parameters
|
|
71
|
+
api_key: str | None = None
|
|
72
|
+
base_url: str | URL | None = None
|
|
73
|
+
timeout: float | Timeout | NotGiven | None = None
|
|
74
|
+
max_retries: int = 10 # Increase default retries for automation reliability
|
|
75
|
+
|
|
76
|
+
def get_client(self) -> AsyncGroq:
|
|
77
|
+
return AsyncGroq(api_key=self.api_key, base_url=self.base_url, timeout=self.timeout, max_retries=self.max_retries)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def provider(self) -> str:
|
|
81
|
+
return 'groq'
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def name(self) -> str:
|
|
85
|
+
return str(self.model)
|
|
86
|
+
|
|
87
|
+
def _get_usage(self, response: ChatCompletion) -> ChatInvokeUsage | None:
|
|
88
|
+
usage = (
|
|
89
|
+
ChatInvokeUsage(
|
|
90
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
91
|
+
completion_tokens=response.usage.completion_tokens,
|
|
92
|
+
total_tokens=response.usage.total_tokens,
|
|
93
|
+
prompt_cached_tokens=None, # Groq doesn't support cached tokens
|
|
94
|
+
prompt_cache_creation_tokens=None,
|
|
95
|
+
prompt_image_tokens=None,
|
|
96
|
+
)
|
|
97
|
+
if response.usage is not None
|
|
98
|
+
else None
|
|
99
|
+
)
|
|
100
|
+
return usage
|
|
101
|
+
|
|
102
|
+
@overload
|
|
103
|
+
async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]: ...
|
|
104
|
+
|
|
105
|
+
@overload
|
|
106
|
+
async def ainvoke(self, messages: list[BaseMessage], output_format: type[T]) -> ChatInvokeCompletion[T]: ...
|
|
107
|
+
|
|
108
|
+
async def ainvoke(
|
|
109
|
+
self, messages: list[BaseMessage], output_format: type[T] | None = None
|
|
110
|
+
) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
|
|
111
|
+
groq_messages = GroqMessageSerializer.serialize_messages(messages)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
if output_format is None:
|
|
115
|
+
return await self._invoke_regular_completion(groq_messages)
|
|
116
|
+
else:
|
|
117
|
+
return await self._invoke_structured_output(groq_messages, output_format)
|
|
118
|
+
|
|
119
|
+
except RateLimitError as e:
|
|
120
|
+
raise ModelRateLimitError(message=e.response.text, status_code=e.response.status_code, model=self.name) from e
|
|
121
|
+
|
|
122
|
+
except APIResponseValidationError as e:
|
|
123
|
+
raise ModelProviderError(message=e.response.text, status_code=e.response.status_code, model=self.name) from e
|
|
124
|
+
|
|
125
|
+
except APIStatusError as e:
|
|
126
|
+
if output_format is None:
|
|
127
|
+
raise ModelProviderError(message=e.response.text, status_code=e.response.status_code, model=self.name) from e
|
|
128
|
+
else:
|
|
129
|
+
try:
|
|
130
|
+
logger.debug(f'Groq failed generation: {e.response.text}; fallback to manual parsing')
|
|
131
|
+
|
|
132
|
+
parsed_response = try_parse_groq_failed_generation(e, output_format)
|
|
133
|
+
|
|
134
|
+
logger.debug('Manual error parsing successful ✅')
|
|
135
|
+
|
|
136
|
+
return ChatInvokeCompletion(
|
|
137
|
+
completion=parsed_response,
|
|
138
|
+
usage=None, # because this is a hacky way to get the outputs
|
|
139
|
+
# TODO: @groq needs to fix their parsers and validators
|
|
140
|
+
)
|
|
141
|
+
except Exception as _:
|
|
142
|
+
raise ModelProviderError(message=str(e), status_code=e.response.status_code, model=self.name) from e
|
|
143
|
+
|
|
144
|
+
except APIError as e:
|
|
145
|
+
raise ModelProviderError(message=e.message, model=self.name) from e
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise ModelProviderError(message=str(e), model=self.name) from e
|
|
148
|
+
|
|
149
|
+
async def _invoke_regular_completion(self, groq_messages) -> ChatInvokeCompletion[str]:
|
|
150
|
+
"""Handle regular completion without structured output."""
|
|
151
|
+
chat_completion = await self.get_client().chat.completions.create(
|
|
152
|
+
messages=groq_messages,
|
|
153
|
+
model=self.model,
|
|
154
|
+
service_tier=self.service_tier,
|
|
155
|
+
temperature=self.temperature,
|
|
156
|
+
top_p=self.top_p,
|
|
157
|
+
seed=self.seed,
|
|
158
|
+
)
|
|
159
|
+
usage = self._get_usage(chat_completion)
|
|
160
|
+
return ChatInvokeCompletion(
|
|
161
|
+
completion=chat_completion.choices[0].message.content or '',
|
|
162
|
+
usage=usage,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def _invoke_structured_output(self, groq_messages, output_format: type[T]) -> ChatInvokeCompletion[T]:
|
|
166
|
+
"""Handle structured output using either tool calling or JSON schema."""
|
|
167
|
+
schema = SchemaOptimizer.create_optimized_json_schema(output_format)
|
|
168
|
+
|
|
169
|
+
if self.model in ToolCallingModels:
|
|
170
|
+
response = await self._invoke_with_tool_calling(groq_messages, output_format, schema)
|
|
171
|
+
else:
|
|
172
|
+
response = await self._invoke_with_json_schema(groq_messages, output_format, schema)
|
|
173
|
+
|
|
174
|
+
if not response.choices[0].message.content:
|
|
175
|
+
raise ModelProviderError(
|
|
176
|
+
message='No content in response',
|
|
177
|
+
status_code=500,
|
|
178
|
+
model=self.name,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parsed_response = output_format.model_validate_json(response.choices[0].message.content)
|
|
182
|
+
usage = self._get_usage(response)
|
|
183
|
+
|
|
184
|
+
return ChatInvokeCompletion(
|
|
185
|
+
completion=parsed_response,
|
|
186
|
+
usage=usage,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def _invoke_with_tool_calling(self, groq_messages, output_format: type[T], schema) -> ChatCompletion:
|
|
190
|
+
"""Handle structured output using tool calling."""
|
|
191
|
+
tool = ChatCompletionToolParam(
|
|
192
|
+
function={
|
|
193
|
+
'name': output_format.__name__,
|
|
194
|
+
'description': f'Extract information in the format of {output_format.__name__}',
|
|
195
|
+
'parameters': schema,
|
|
196
|
+
},
|
|
197
|
+
type='function',
|
|
198
|
+
)
|
|
199
|
+
tool_choice: ChatCompletionToolChoiceOptionParam = 'required'
|
|
200
|
+
|
|
201
|
+
return await self.get_client().chat.completions.create(
|
|
202
|
+
model=self.model,
|
|
203
|
+
messages=groq_messages,
|
|
204
|
+
temperature=self.temperature,
|
|
205
|
+
top_p=self.top_p,
|
|
206
|
+
seed=self.seed,
|
|
207
|
+
tools=[tool],
|
|
208
|
+
tool_choice=tool_choice,
|
|
209
|
+
service_tier=self.service_tier,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async def _invoke_with_json_schema(self, groq_messages, output_format: type[T], schema) -> ChatCompletion:
|
|
213
|
+
"""Handle structured output using JSON schema."""
|
|
214
|
+
return await self.get_client().chat.completions.create(
|
|
215
|
+
model=self.model,
|
|
216
|
+
messages=groq_messages,
|
|
217
|
+
temperature=self.temperature,
|
|
218
|
+
top_p=self.top_p,
|
|
219
|
+
seed=self.seed,
|
|
220
|
+
response_format=ResponseFormatResponseFormatJsonSchema(
|
|
221
|
+
json_schema=ResponseFormatResponseFormatJsonSchemaJsonSchema(
|
|
222
|
+
name=output_format.__name__,
|
|
223
|
+
description='Model output schema',
|
|
224
|
+
schema=schema,
|
|
225
|
+
),
|
|
226
|
+
type='json_schema',
|
|
227
|
+
),
|
|
228
|
+
service_tier=self.service_tier,
|
|
229
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
from groq import APIStatusError
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T', bound=BaseModel)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ParseFailedGenerationError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def try_parse_groq_failed_generation(
|
|
19
|
+
error: APIStatusError,
|
|
20
|
+
output_format: type[T],
|
|
21
|
+
) -> T:
|
|
22
|
+
"""Extract JSON from model output, handling both plain JSON and code-block-wrapped JSON."""
|
|
23
|
+
try:
|
|
24
|
+
content = error.body['error']['failed_generation'] # type: ignore
|
|
25
|
+
|
|
26
|
+
# If content is wrapped in code blocks, extract just the JSON part
|
|
27
|
+
if '```' in content:
|
|
28
|
+
# Find the JSON content between code blocks
|
|
29
|
+
content = content.split('```')[1]
|
|
30
|
+
# Remove language identifier if present (e.g., 'json\n')
|
|
31
|
+
if '\n' in content:
|
|
32
|
+
content = content.split('\n', 1)[1]
|
|
33
|
+
|
|
34
|
+
# remove html-like tags before the first { and after the last }
|
|
35
|
+
# This handles cases like <|header_start|>assistant<|header_end|> and <function=AgentOutput>
|
|
36
|
+
# Only remove content before { if content doesn't already start with {
|
|
37
|
+
if not content.strip().startswith('{'):
|
|
38
|
+
content = re.sub(r'^.*?(?=\{)', '', content, flags=re.DOTALL)
|
|
39
|
+
|
|
40
|
+
# Remove common HTML-like tags and patterns at the end, but be more conservative
|
|
41
|
+
# Look for patterns like </function>, <|header_start|>, etc. after the JSON
|
|
42
|
+
content = re.sub(r'\}(\s*<[^>]*>.*?$)', '}', content, flags=re.DOTALL)
|
|
43
|
+
content = re.sub(r'\}(\s*<\|[^|]*\|>.*?$)', '}', content, flags=re.DOTALL)
|
|
44
|
+
|
|
45
|
+
# Handle extra characters after the JSON, including stray braces
|
|
46
|
+
# Find the position of the last } that would close the main JSON object
|
|
47
|
+
content = content.strip()
|
|
48
|
+
|
|
49
|
+
if content.endswith('}'):
|
|
50
|
+
# Try to parse and see if we get valid JSON
|
|
51
|
+
try:
|
|
52
|
+
json.loads(content)
|
|
53
|
+
except json.JSONDecodeError:
|
|
54
|
+
# If parsing fails, try to find the correct end of the JSON
|
|
55
|
+
# by counting braces and removing anything after the balanced JSON
|
|
56
|
+
brace_count = 0
|
|
57
|
+
last_valid_pos = -1
|
|
58
|
+
for i, char in enumerate(content):
|
|
59
|
+
if char == '{':
|
|
60
|
+
brace_count += 1
|
|
61
|
+
elif char == '}':
|
|
62
|
+
brace_count -= 1
|
|
63
|
+
if brace_count == 0:
|
|
64
|
+
last_valid_pos = i + 1
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
if last_valid_pos > 0:
|
|
68
|
+
content = content[:last_valid_pos]
|
|
69
|
+
|
|
70
|
+
# Fix control characters in JSON strings before parsing
|
|
71
|
+
# This handles cases where literal control characters appear in JSON values
|
|
72
|
+
content = _fix_control_characters_in_json(content)
|
|
73
|
+
|
|
74
|
+
# Parse the cleaned content
|
|
75
|
+
result_dict = json.loads(content)
|
|
76
|
+
|
|
77
|
+
# some models occasionally respond with a list containing one dict: https://github.com/browser-use/browser-use/issues/1458
|
|
78
|
+
if isinstance(result_dict, list) and len(result_dict) == 1 and isinstance(result_dict[0], dict):
|
|
79
|
+
result_dict = result_dict[0]
|
|
80
|
+
|
|
81
|
+
logger.debug(f'Successfully parsed model output: {result_dict}')
|
|
82
|
+
return output_format.model_validate(result_dict)
|
|
83
|
+
|
|
84
|
+
except KeyError as e:
|
|
85
|
+
raise ParseFailedGenerationError(e) from e
|
|
86
|
+
|
|
87
|
+
except json.JSONDecodeError as e:
|
|
88
|
+
logger.warning(f'Failed to parse model output: {content} {str(e)}')
|
|
89
|
+
raise ValueError(f'Could not parse response. {str(e)}')
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise ParseFailedGenerationError(error.response.text) from e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fix_control_characters_in_json(content: str) -> str:
|
|
96
|
+
"""Fix control characters in JSON string values to make them valid JSON."""
|
|
97
|
+
try:
|
|
98
|
+
# First try to parse as-is to see if it's already valid
|
|
99
|
+
json.loads(content)
|
|
100
|
+
return content
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# More sophisticated approach: only escape control characters inside string values
|
|
105
|
+
# while preserving JSON structure formatting
|
|
106
|
+
|
|
107
|
+
result = []
|
|
108
|
+
i = 0
|
|
109
|
+
in_string = False
|
|
110
|
+
escaped = False
|
|
111
|
+
|
|
112
|
+
while i < len(content):
|
|
113
|
+
char = content[i]
|
|
114
|
+
|
|
115
|
+
if not in_string:
|
|
116
|
+
# Outside of string - check if we're entering a string
|
|
117
|
+
if char == '"':
|
|
118
|
+
in_string = True
|
|
119
|
+
result.append(char)
|
|
120
|
+
else:
|
|
121
|
+
# Inside string - handle escaping and control characters
|
|
122
|
+
if escaped:
|
|
123
|
+
# Previous character was backslash, so this character is escaped
|
|
124
|
+
result.append(char)
|
|
125
|
+
escaped = False
|
|
126
|
+
elif char == '\\':
|
|
127
|
+
# This is an escape character
|
|
128
|
+
result.append(char)
|
|
129
|
+
escaped = True
|
|
130
|
+
elif char == '"':
|
|
131
|
+
# End of string
|
|
132
|
+
result.append(char)
|
|
133
|
+
in_string = False
|
|
134
|
+
elif char == '\n':
|
|
135
|
+
# Literal newline inside string - escape it
|
|
136
|
+
result.append('\\n')
|
|
137
|
+
elif char == '\r':
|
|
138
|
+
# Literal carriage return inside string - escape it
|
|
139
|
+
result.append('\\r')
|
|
140
|
+
elif char == '\t':
|
|
141
|
+
# Literal tab inside string - escape it
|
|
142
|
+
result.append('\\t')
|
|
143
|
+
elif char == '\b':
|
|
144
|
+
# Literal backspace inside string - escape it
|
|
145
|
+
result.append('\\b')
|
|
146
|
+
elif char == '\f':
|
|
147
|
+
# Literal form feed inside string - escape it
|
|
148
|
+
result.append('\\f')
|
|
149
|
+
elif ord(char) < 32:
|
|
150
|
+
# Other control characters inside string - convert to unicode escape
|
|
151
|
+
result.append(f'\\u{ord(char):04x}')
|
|
152
|
+
else:
|
|
153
|
+
# Normal character inside string
|
|
154
|
+
result.append(char)
|
|
155
|
+
|
|
156
|
+
i += 1
|
|
157
|
+
|
|
158
|
+
return ''.join(result)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import overload
|
|
2
|
+
|
|
3
|
+
from groq.types.chat import (
|
|
4
|
+
ChatCompletionAssistantMessageParam,
|
|
5
|
+
ChatCompletionContentPartImageParam,
|
|
6
|
+
ChatCompletionContentPartTextParam,
|
|
7
|
+
ChatCompletionMessageParam,
|
|
8
|
+
ChatCompletionMessageToolCallParam,
|
|
9
|
+
ChatCompletionSystemMessageParam,
|
|
10
|
+
ChatCompletionUserMessageParam,
|
|
11
|
+
)
|
|
12
|
+
from groq.types.chat.chat_completion_content_part_image_param import ImageURL
|
|
13
|
+
from groq.types.chat.chat_completion_message_tool_call_param import Function
|
|
14
|
+
|
|
15
|
+
from browser_use.llm.messages import (
|
|
16
|
+
AssistantMessage,
|
|
17
|
+
BaseMessage,
|
|
18
|
+
ContentPartImageParam,
|
|
19
|
+
ContentPartRefusalParam,
|
|
20
|
+
ContentPartTextParam,
|
|
21
|
+
SystemMessage,
|
|
22
|
+
ToolCall,
|
|
23
|
+
UserMessage,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GroqMessageSerializer:
|
|
28
|
+
"""Serializer for converting between custom message types and OpenAI message param types."""
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _serialize_content_part_text(part: ContentPartTextParam) -> ChatCompletionContentPartTextParam:
|
|
32
|
+
return ChatCompletionContentPartTextParam(text=part.text, type='text')
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _serialize_content_part_image(part: ContentPartImageParam) -> ChatCompletionContentPartImageParam:
|
|
36
|
+
return ChatCompletionContentPartImageParam(
|
|
37
|
+
image_url=ImageURL(url=part.image_url.url, detail=part.image_url.detail),
|
|
38
|
+
type='image_url',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _serialize_user_content(
|
|
43
|
+
content: str | list[ContentPartTextParam | ContentPartImageParam],
|
|
44
|
+
) -> str | list[ChatCompletionContentPartTextParam | ChatCompletionContentPartImageParam]:
|
|
45
|
+
"""Serialize content for user messages (text and images allowed)."""
|
|
46
|
+
if isinstance(content, str):
|
|
47
|
+
return content
|
|
48
|
+
|
|
49
|
+
serialized_parts: list[ChatCompletionContentPartTextParam | ChatCompletionContentPartImageParam] = []
|
|
50
|
+
for part in content:
|
|
51
|
+
if part.type == 'text':
|
|
52
|
+
serialized_parts.append(GroqMessageSerializer._serialize_content_part_text(part))
|
|
53
|
+
elif part.type == 'image_url':
|
|
54
|
+
serialized_parts.append(GroqMessageSerializer._serialize_content_part_image(part))
|
|
55
|
+
return serialized_parts
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _serialize_system_content(
|
|
59
|
+
content: str | list[ContentPartTextParam],
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Serialize content for system messages (text only)."""
|
|
62
|
+
if isinstance(content, str):
|
|
63
|
+
return content
|
|
64
|
+
|
|
65
|
+
serialized_parts: list[str] = []
|
|
66
|
+
for part in content:
|
|
67
|
+
if part.type == 'text':
|
|
68
|
+
serialized_parts.append(GroqMessageSerializer._serialize_content_part_text(part)['text'])
|
|
69
|
+
|
|
70
|
+
return '\n'.join(serialized_parts)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _serialize_assistant_content(
|
|
74
|
+
content: str | list[ContentPartTextParam | ContentPartRefusalParam] | None,
|
|
75
|
+
) -> str | None:
|
|
76
|
+
"""Serialize content for assistant messages (text and refusal allowed)."""
|
|
77
|
+
if content is None:
|
|
78
|
+
return None
|
|
79
|
+
if isinstance(content, str):
|
|
80
|
+
return content
|
|
81
|
+
|
|
82
|
+
serialized_parts: list[str] = []
|
|
83
|
+
for part in content:
|
|
84
|
+
if part.type == 'text':
|
|
85
|
+
serialized_parts.append(GroqMessageSerializer._serialize_content_part_text(part)['text'])
|
|
86
|
+
|
|
87
|
+
return '\n'.join(serialized_parts)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _serialize_tool_call(tool_call: ToolCall) -> ChatCompletionMessageToolCallParam:
|
|
91
|
+
return ChatCompletionMessageToolCallParam(
|
|
92
|
+
id=tool_call.id,
|
|
93
|
+
function=Function(name=tool_call.function.name, arguments=tool_call.function.arguments),
|
|
94
|
+
type='function',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# endregion
|
|
98
|
+
|
|
99
|
+
# region - Serialize overloads
|
|
100
|
+
@overload
|
|
101
|
+
@staticmethod
|
|
102
|
+
def serialize(message: UserMessage) -> ChatCompletionUserMessageParam: ...
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
@staticmethod
|
|
106
|
+
def serialize(message: SystemMessage) -> ChatCompletionSystemMessageParam: ...
|
|
107
|
+
|
|
108
|
+
@overload
|
|
109
|
+
@staticmethod
|
|
110
|
+
def serialize(message: AssistantMessage) -> ChatCompletionAssistantMessageParam: ...
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def serialize(message: BaseMessage) -> ChatCompletionMessageParam:
|
|
114
|
+
"""Serialize a custom message to an OpenAI message param."""
|
|
115
|
+
|
|
116
|
+
if isinstance(message, UserMessage):
|
|
117
|
+
user_result: ChatCompletionUserMessageParam = {
|
|
118
|
+
'role': 'user',
|
|
119
|
+
'content': GroqMessageSerializer._serialize_user_content(message.content),
|
|
120
|
+
}
|
|
121
|
+
if message.name is not None:
|
|
122
|
+
user_result['name'] = message.name
|
|
123
|
+
return user_result
|
|
124
|
+
|
|
125
|
+
elif isinstance(message, SystemMessage):
|
|
126
|
+
system_result: ChatCompletionSystemMessageParam = {
|
|
127
|
+
'role': 'system',
|
|
128
|
+
'content': GroqMessageSerializer._serialize_system_content(message.content),
|
|
129
|
+
}
|
|
130
|
+
if message.name is not None:
|
|
131
|
+
system_result['name'] = message.name
|
|
132
|
+
return system_result
|
|
133
|
+
|
|
134
|
+
elif isinstance(message, AssistantMessage):
|
|
135
|
+
# Handle content serialization
|
|
136
|
+
content = None
|
|
137
|
+
if message.content is not None:
|
|
138
|
+
content = GroqMessageSerializer._serialize_assistant_content(message.content)
|
|
139
|
+
|
|
140
|
+
assistant_result: ChatCompletionAssistantMessageParam = {'role': 'assistant'}
|
|
141
|
+
|
|
142
|
+
# Only add content if it's not None
|
|
143
|
+
if content is not None:
|
|
144
|
+
assistant_result['content'] = content
|
|
145
|
+
|
|
146
|
+
if message.name is not None:
|
|
147
|
+
assistant_result['name'] = message.name
|
|
148
|
+
|
|
149
|
+
if message.tool_calls:
|
|
150
|
+
assistant_result['tool_calls'] = [GroqMessageSerializer._serialize_tool_call(tc) for tc in message.tool_calls]
|
|
151
|
+
|
|
152
|
+
return assistant_result
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(f'Unknown message type: {type(message)}')
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def serialize_messages(messages: list[BaseMessage]) -> list[ChatCompletionMessageParam]:
|
|
159
|
+
return [GroqMessageSerializer.serialize(m) for m in messages]
|