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.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. 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]