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,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]