camel-ai 0.2.26__py3-none-any.whl → 0.2.27__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.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/models/__init__.py +4 -0
- camel/models/azure_openai_model.py +88 -8
- camel/models/model_factory.py +3 -0
- camel/models/openai_compatible_model.py +88 -8
- camel/models/volcano_model.py +100 -0
- camel/toolkits/__init__.py +2 -2
- camel/toolkits/{web_toolkit.py → browser_toolkit.py} +143 -76
- camel/toolkits/search_toolkit.py +63 -0
- camel/types/enums.py +7 -0
- {camel_ai-0.2.26.dist-info → camel_ai-0.2.27.dist-info}/METADATA +1 -1
- {camel_ai-0.2.26.dist-info → camel_ai-0.2.27.dist-info}/RECORD +14 -13
- {camel_ai-0.2.26.dist-info → camel_ai-0.2.27.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.26.dist-info → camel_ai-0.2.27.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
camel/models/__init__.py
CHANGED
|
@@ -37,9 +37,11 @@ from .qwen_model import QwenModel
|
|
|
37
37
|
from .reka_model import RekaModel
|
|
38
38
|
from .samba_model import SambaModel
|
|
39
39
|
from .sglang_model import SGLangModel
|
|
40
|
+
from .siliconflow_model import SiliconFlowModel
|
|
40
41
|
from .stub_model import StubModel
|
|
41
42
|
from .togetherai_model import TogetherAIModel
|
|
42
43
|
from .vllm_model import VLLMModel
|
|
44
|
+
from .volcano_model import VolcanoModel
|
|
43
45
|
from .yi_model import YiModel
|
|
44
46
|
from .zhipuai_model import ZhipuAIModel
|
|
45
47
|
|
|
@@ -76,4 +78,6 @@ __all__ = [
|
|
|
76
78
|
'MoonshotModel',
|
|
77
79
|
'AIMLModel',
|
|
78
80
|
'BaseAudioModel',
|
|
81
|
+
'SiliconFlowModel',
|
|
82
|
+
'VolcanoModel',
|
|
79
83
|
]
|
|
@@ -128,18 +128,23 @@ class AzureOpenAIModel(BaseModelBackend):
|
|
|
128
128
|
Args:
|
|
129
129
|
messages (List[OpenAIMessage]): Message list with the chat history
|
|
130
130
|
in OpenAI API format.
|
|
131
|
+
response_format (Optional[Type[BaseModel]]): The format of the
|
|
132
|
+
response.
|
|
133
|
+
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
|
|
134
|
+
use for the request.
|
|
131
135
|
|
|
132
136
|
Returns:
|
|
133
137
|
Union[ChatCompletion, Stream[ChatCompletionChunk]]:
|
|
134
138
|
`ChatCompletion` in the non-stream mode, or
|
|
135
139
|
`Stream[ChatCompletionChunk]` in the stream mode.
|
|
136
140
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
model=self.azure_deployment_name, # type:ignore[arg-type]
|
|
140
|
-
**self.model_config_dict,
|
|
141
|
+
response_format = response_format or self.model_config_dict.get(
|
|
142
|
+
"response_format", None
|
|
141
143
|
)
|
|
142
|
-
|
|
144
|
+
if response_format:
|
|
145
|
+
return self._request_parse(messages, response_format, tools)
|
|
146
|
+
else:
|
|
147
|
+
return self._request_chat_completion(messages, tools)
|
|
143
148
|
|
|
144
149
|
async def _arun(
|
|
145
150
|
self,
|
|
@@ -152,18 +157,93 @@ class AzureOpenAIModel(BaseModelBackend):
|
|
|
152
157
|
Args:
|
|
153
158
|
messages (List[OpenAIMessage]): Message list with the chat history
|
|
154
159
|
in OpenAI API format.
|
|
160
|
+
response_format (Optional[Type[BaseModel]]): The format of the
|
|
161
|
+
response.
|
|
162
|
+
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
|
|
163
|
+
use for the request.
|
|
155
164
|
|
|
156
165
|
Returns:
|
|
157
166
|
Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
|
|
158
167
|
`ChatCompletion` in the non-stream mode, or
|
|
159
168
|
`AsyncStream[ChatCompletionChunk]` in the stream mode.
|
|
160
169
|
"""
|
|
161
|
-
|
|
170
|
+
response_format = response_format or self.model_config_dict.get(
|
|
171
|
+
"response_format", None
|
|
172
|
+
)
|
|
173
|
+
if response_format:
|
|
174
|
+
return await self._arequest_parse(messages, response_format, tools)
|
|
175
|
+
else:
|
|
176
|
+
return await self._arequest_chat_completion(messages, tools)
|
|
177
|
+
|
|
178
|
+
def _request_chat_completion(
|
|
179
|
+
self,
|
|
180
|
+
messages: List[OpenAIMessage],
|
|
181
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
182
|
+
) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
|
|
183
|
+
request_config = self.model_config_dict.copy()
|
|
184
|
+
|
|
185
|
+
if tools:
|
|
186
|
+
request_config["tools"] = tools
|
|
187
|
+
|
|
188
|
+
return self._client.chat.completions.create(
|
|
189
|
+
messages=messages,
|
|
190
|
+
model=self.azure_deployment_name, # type:ignore[arg-type]
|
|
191
|
+
**request_config,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def _arequest_chat_completion(
|
|
195
|
+
self,
|
|
196
|
+
messages: List[OpenAIMessage],
|
|
197
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
198
|
+
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
|
|
199
|
+
request_config = self.model_config_dict.copy()
|
|
200
|
+
|
|
201
|
+
if tools:
|
|
202
|
+
request_config["tools"] = tools
|
|
203
|
+
|
|
204
|
+
return await self._async_client.chat.completions.create(
|
|
205
|
+
messages=messages,
|
|
206
|
+
model=self.azure_deployment_name, # type:ignore[arg-type]
|
|
207
|
+
**request_config,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _request_parse(
|
|
211
|
+
self,
|
|
212
|
+
messages: List[OpenAIMessage],
|
|
213
|
+
response_format: Type[BaseModel],
|
|
214
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
215
|
+
) -> ChatCompletion:
|
|
216
|
+
request_config = self.model_config_dict.copy()
|
|
217
|
+
|
|
218
|
+
request_config["response_format"] = response_format
|
|
219
|
+
request_config.pop("stream", None)
|
|
220
|
+
if tools is not None:
|
|
221
|
+
request_config["tools"] = tools
|
|
222
|
+
|
|
223
|
+
return self._client.beta.chat.completions.parse(
|
|
224
|
+
messages=messages,
|
|
225
|
+
model=self.azure_deployment_name, # type:ignore[arg-type]
|
|
226
|
+
**request_config,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def _arequest_parse(
|
|
230
|
+
self,
|
|
231
|
+
messages: List[OpenAIMessage],
|
|
232
|
+
response_format: Type[BaseModel],
|
|
233
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
234
|
+
) -> ChatCompletion:
|
|
235
|
+
request_config = self.model_config_dict.copy()
|
|
236
|
+
|
|
237
|
+
request_config["response_format"] = response_format
|
|
238
|
+
request_config.pop("stream", None)
|
|
239
|
+
if tools is not None:
|
|
240
|
+
request_config["tools"] = tools
|
|
241
|
+
|
|
242
|
+
return await self._async_client.beta.chat.completions.parse(
|
|
162
243
|
messages=messages,
|
|
163
244
|
model=self.azure_deployment_name, # type:ignore[arg-type]
|
|
164
|
-
**
|
|
245
|
+
**request_config,
|
|
165
246
|
)
|
|
166
|
-
return response
|
|
167
247
|
|
|
168
248
|
def check_model_config(self):
|
|
169
249
|
r"""Check whether the model configuration contains any
|
camel/models/model_factory.py
CHANGED
|
@@ -37,6 +37,7 @@ from camel.models.siliconflow_model import SiliconFlowModel
|
|
|
37
37
|
from camel.models.stub_model import StubModel
|
|
38
38
|
from camel.models.togetherai_model import TogetherAIModel
|
|
39
39
|
from camel.models.vllm_model import VLLMModel
|
|
40
|
+
from camel.models.volcano_model import VolcanoModel
|
|
40
41
|
from camel.models.yi_model import YiModel
|
|
41
42
|
from camel.models.zhipuai_model import ZhipuAIModel
|
|
42
43
|
from camel.types import ModelPlatformType, ModelType, UnifiedModelType
|
|
@@ -107,6 +108,8 @@ class ModelFactory:
|
|
|
107
108
|
model_class = SiliconFlowModel
|
|
108
109
|
elif model_platform.is_aiml:
|
|
109
110
|
model_class = AIMLModel
|
|
111
|
+
elif model_platform.is_volcano:
|
|
112
|
+
model_class = VolcanoModel
|
|
110
113
|
|
|
111
114
|
elif model_platform.is_openai and model_type.is_openai:
|
|
112
115
|
model_class = OpenAIModel
|
|
@@ -86,18 +86,23 @@ class OpenAICompatibleModel(BaseModelBackend):
|
|
|
86
86
|
Args:
|
|
87
87
|
messages (List[OpenAIMessage]): Message list with the chat history
|
|
88
88
|
in OpenAI API format.
|
|
89
|
+
response_format (Optional[Type[BaseModel]]): The format of the
|
|
90
|
+
response.
|
|
91
|
+
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
|
|
92
|
+
use for the request.
|
|
89
93
|
|
|
90
94
|
Returns:
|
|
91
95
|
Union[ChatCompletion, Stream[ChatCompletionChunk]]:
|
|
92
96
|
`ChatCompletion` in the non-stream mode, or
|
|
93
97
|
`Stream[ChatCompletionChunk]` in the stream mode.
|
|
94
98
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
model=self.model_type,
|
|
98
|
-
**self.model_config_dict,
|
|
99
|
+
response_format = response_format or self.model_config_dict.get(
|
|
100
|
+
"response_format", None
|
|
99
101
|
)
|
|
100
|
-
|
|
102
|
+
if response_format:
|
|
103
|
+
return self._request_parse(messages, response_format, tools)
|
|
104
|
+
else:
|
|
105
|
+
return self._request_chat_completion(messages, tools)
|
|
101
106
|
|
|
102
107
|
async def _arun(
|
|
103
108
|
self,
|
|
@@ -110,18 +115,93 @@ class OpenAICompatibleModel(BaseModelBackend):
|
|
|
110
115
|
Args:
|
|
111
116
|
messages (List[OpenAIMessage]): Message list with the chat history
|
|
112
117
|
in OpenAI API format.
|
|
118
|
+
response_format (Optional[Type[BaseModel]]): The format of the
|
|
119
|
+
response.
|
|
120
|
+
tools (Optional[List[Dict[str, Any]]]): The schema of the tools to
|
|
121
|
+
use for the request.
|
|
113
122
|
|
|
114
123
|
Returns:
|
|
115
124
|
Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
|
|
116
125
|
`ChatCompletion` in the non-stream mode, or
|
|
117
126
|
`AsyncStream[ChatCompletionChunk]` in the stream mode.
|
|
118
127
|
"""
|
|
119
|
-
|
|
128
|
+
response_format = response_format or self.model_config_dict.get(
|
|
129
|
+
"response_format", None
|
|
130
|
+
)
|
|
131
|
+
if response_format:
|
|
132
|
+
return await self._arequest_parse(messages, response_format, tools)
|
|
133
|
+
else:
|
|
134
|
+
return await self._arequest_chat_completion(messages, tools)
|
|
135
|
+
|
|
136
|
+
def _request_chat_completion(
|
|
137
|
+
self,
|
|
138
|
+
messages: List[OpenAIMessage],
|
|
139
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
140
|
+
) -> Union[ChatCompletion, Stream[ChatCompletionChunk]]:
|
|
141
|
+
request_config = self.model_config_dict.copy()
|
|
142
|
+
|
|
143
|
+
if tools:
|
|
144
|
+
request_config["tools"] = tools
|
|
145
|
+
|
|
146
|
+
return self._client.chat.completions.create(
|
|
147
|
+
messages=messages,
|
|
148
|
+
model=self.model_type,
|
|
149
|
+
**request_config,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def _arequest_chat_completion(
|
|
153
|
+
self,
|
|
154
|
+
messages: List[OpenAIMessage],
|
|
155
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
156
|
+
) -> Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]:
|
|
157
|
+
request_config = self.model_config_dict.copy()
|
|
158
|
+
|
|
159
|
+
if tools:
|
|
160
|
+
request_config["tools"] = tools
|
|
161
|
+
|
|
162
|
+
return await self._async_client.chat.completions.create(
|
|
163
|
+
messages=messages,
|
|
164
|
+
model=self.model_type,
|
|
165
|
+
**request_config,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _request_parse(
|
|
169
|
+
self,
|
|
170
|
+
messages: List[OpenAIMessage],
|
|
171
|
+
response_format: Type[BaseModel],
|
|
172
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
173
|
+
) -> ChatCompletion:
|
|
174
|
+
request_config = self.model_config_dict.copy()
|
|
175
|
+
|
|
176
|
+
request_config["response_format"] = response_format
|
|
177
|
+
request_config.pop("stream", None)
|
|
178
|
+
if tools is not None:
|
|
179
|
+
request_config["tools"] = tools
|
|
180
|
+
|
|
181
|
+
return self._client.beta.chat.completions.parse(
|
|
182
|
+
messages=messages,
|
|
183
|
+
model=self.model_type,
|
|
184
|
+
**request_config,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async def _arequest_parse(
|
|
188
|
+
self,
|
|
189
|
+
messages: List[OpenAIMessage],
|
|
190
|
+
response_format: Type[BaseModel],
|
|
191
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
192
|
+
) -> ChatCompletion:
|
|
193
|
+
request_config = self.model_config_dict.copy()
|
|
194
|
+
|
|
195
|
+
request_config["response_format"] = response_format
|
|
196
|
+
request_config.pop("stream", None)
|
|
197
|
+
if tools is not None:
|
|
198
|
+
request_config["tools"] = tools
|
|
199
|
+
|
|
200
|
+
return await self._async_client.beta.chat.completions.parse(
|
|
120
201
|
messages=messages,
|
|
121
202
|
model=self.model_type,
|
|
122
|
-
**
|
|
203
|
+
**request_config,
|
|
123
204
|
)
|
|
124
|
-
return response
|
|
125
205
|
|
|
126
206
|
@property
|
|
127
207
|
def token_counter(self) -> BaseTokenCounter:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any, Dict, Optional, Union
|
|
17
|
+
|
|
18
|
+
from camel.configs import OPENAI_API_PARAMS
|
|
19
|
+
from camel.models.openai_compatible_model import OpenAICompatibleModel
|
|
20
|
+
from camel.types import ModelType
|
|
21
|
+
from camel.utils import (
|
|
22
|
+
BaseTokenCounter,
|
|
23
|
+
OpenAITokenCounter,
|
|
24
|
+
api_keys_required,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class VolcanoModel(OpenAICompatibleModel):
|
|
29
|
+
r"""Volcano Engine API in a unified BaseModelBackend interface.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
model_type (Union[ModelType, str]): Model for which a backend is
|
|
33
|
+
created.
|
|
34
|
+
model_config_dict (Optional[Dict[str, Any]], optional): A dictionary
|
|
35
|
+
that will be fed into the API call. If
|
|
36
|
+
:obj:`None`, :obj:`{}` will be used. (default: :obj:`None`)
|
|
37
|
+
api_key (Optional[str], optional): The API key for authenticating with
|
|
38
|
+
the Volcano Engine service. (default: :obj:`None`)
|
|
39
|
+
url (Optional[str], optional): The url to the Volcano Engine service.
|
|
40
|
+
(default: :obj:`https://ark.cn-beijing.volces.com/api/v3`)
|
|
41
|
+
token_counter (Optional[BaseTokenCounter], optional): Token counter to
|
|
42
|
+
use for the model. If not provided, :obj:`OpenAITokenCounter`
|
|
43
|
+
will be used. (default: :obj:`None`)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@api_keys_required(
|
|
47
|
+
[
|
|
48
|
+
("api_key", "VOLCANO_API_KEY"),
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
model_type: Union[ModelType, str],
|
|
54
|
+
model_config_dict: Optional[Dict[str, Any]] = None,
|
|
55
|
+
api_key: Optional[str] = None,
|
|
56
|
+
url: Optional[str] = None,
|
|
57
|
+
token_counter: Optional[BaseTokenCounter] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
if model_config_dict is None:
|
|
60
|
+
model_config_dict = {}
|
|
61
|
+
|
|
62
|
+
api_key = api_key or os.environ.get("VOLCANO_API_KEY")
|
|
63
|
+
url = (
|
|
64
|
+
url
|
|
65
|
+
or os.environ.get("VOLCANO_API_BASE_URL")
|
|
66
|
+
or "https://ark.cn-beijing.volces.com/api/v3"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
super().__init__(
|
|
70
|
+
model_type, model_config_dict, api_key, url, token_counter
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def token_counter(self) -> BaseTokenCounter:
|
|
75
|
+
r"""Initialize the token counter for the model backend.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
BaseTokenCounter: The token counter following the model's
|
|
79
|
+
tokenization style.
|
|
80
|
+
"""
|
|
81
|
+
if not self._token_counter:
|
|
82
|
+
# Use OpenAI token counter as an approximation
|
|
83
|
+
self._token_counter = OpenAITokenCounter(ModelType.GPT_4O_MINI)
|
|
84
|
+
return self._token_counter
|
|
85
|
+
|
|
86
|
+
def check_model_config(self):
|
|
87
|
+
r"""Check whether the model configuration is valid for Volcano
|
|
88
|
+
model backends.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If the model configuration dictionary contains any
|
|
92
|
+
unexpected arguments to Volcano API.
|
|
93
|
+
"""
|
|
94
|
+
# Using OpenAI API params as Volcano Engine API is OpenAI-compatible
|
|
95
|
+
for param in self.model_config_dict:
|
|
96
|
+
if param not in OPENAI_API_PARAMS:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Unexpected argument `{param}` is "
|
|
99
|
+
"input into Volcano model backend."
|
|
100
|
+
)
|
camel/toolkits/__init__.py
CHANGED
|
@@ -55,7 +55,7 @@ from .excel_toolkit import ExcelToolkit
|
|
|
55
55
|
from .video_analysis_toolkit import VideoAnalysisToolkit
|
|
56
56
|
from .image_analysis_toolkit import ImageAnalysisToolkit
|
|
57
57
|
from .mcp_toolkit import MCPToolkit
|
|
58
|
-
from .
|
|
58
|
+
from .browser_toolkit import BrowserToolkit
|
|
59
59
|
from .file_write_toolkit import FileWriteToolkit
|
|
60
60
|
from .terminal_toolkit import TerminalToolkit
|
|
61
61
|
from .pubmed_toolkit import PubMedToolkit
|
|
@@ -102,7 +102,7 @@ __all__ = [
|
|
|
102
102
|
'ExcelToolkit',
|
|
103
103
|
'VideoAnalysisToolkit',
|
|
104
104
|
'ImageAnalysisToolkit',
|
|
105
|
-
'
|
|
105
|
+
'BrowserToolkit',
|
|
106
106
|
'FileWriteToolkit',
|
|
107
107
|
'TerminalToolkit',
|
|
108
108
|
'PubMedToolkit',
|
|
@@ -92,7 +92,7 @@ ACTION_WITH_FEEDBACK_LIST = [
|
|
|
92
92
|
]
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
#
|
|
95
|
+
# Code from magentic-one
|
|
96
96
|
class DOMRectangle(TypedDict):
|
|
97
97
|
x: Union[int, float]
|
|
98
98
|
y: Union[int, float]
|
|
@@ -127,21 +127,36 @@ class InteractiveRegion(TypedDict):
|
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def _get_str(d: Any, k: str) -> str:
|
|
130
|
+
r"""Safely retrieve a string value from a dictionary."""
|
|
131
|
+
if k not in d:
|
|
132
|
+
raise KeyError(f"Missing required key: '{k}'")
|
|
130
133
|
val = d[k]
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
if isinstance(val, str):
|
|
135
|
+
return val
|
|
136
|
+
raise TypeError(
|
|
137
|
+
f"Expected a string for key '{k}', " f"but got {type(val).__name__}"
|
|
138
|
+
)
|
|
133
139
|
|
|
134
140
|
|
|
135
141
|
def _get_number(d: Any, k: str) -> Union[int, float]:
|
|
142
|
+
r"""Safely retrieve a number (int or float) from a dictionary"""
|
|
136
143
|
val = d[k]
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
if isinstance(val, (int, float)):
|
|
145
|
+
return val
|
|
146
|
+
raise TypeError(
|
|
147
|
+
f"Expected a number (int/float) for key "
|
|
148
|
+
f"'{k}', but got {type(val).__name__}"
|
|
149
|
+
)
|
|
139
150
|
|
|
140
151
|
|
|
141
152
|
def _get_bool(d: Any, k: str) -> bool:
|
|
153
|
+
r"""Safely retrieve a boolean value from a dictionary."""
|
|
142
154
|
val = d[k]
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
if isinstance(val, bool):
|
|
156
|
+
return val
|
|
157
|
+
raise TypeError(
|
|
158
|
+
f"Expected a boolean for key '{k}', " f"but got {type(val).__name__}"
|
|
159
|
+
)
|
|
145
160
|
|
|
146
161
|
|
|
147
162
|
def _parse_json_output(text: str) -> Dict[str, Any]:
|
|
@@ -208,7 +223,8 @@ def _reload_image(image: Image.Image):
|
|
|
208
223
|
return Image.open(buffer)
|
|
209
224
|
|
|
210
225
|
|
|
211
|
-
def
|
|
226
|
+
def dom_rectangle_from_dict(rect: Dict[str, Any]) -> DOMRectangle:
|
|
227
|
+
r"""Create a DOMRectangle object from a dictionary."""
|
|
212
228
|
return DOMRectangle(
|
|
213
229
|
x=_get_number(rect, "x"),
|
|
214
230
|
y=_get_number(rect, "y"),
|
|
@@ -221,10 +237,11 @@ def domrectangle_from_dict(rect: Dict[str, Any]) -> DOMRectangle:
|
|
|
221
237
|
)
|
|
222
238
|
|
|
223
239
|
|
|
224
|
-
def
|
|
240
|
+
def interactive_region_from_dict(region: Dict[str, Any]) -> InteractiveRegion:
|
|
241
|
+
r"""Create an :class:`InteractiveRegion` object from a dictionary."""
|
|
225
242
|
typed_rects: List[DOMRectangle] = []
|
|
226
243
|
for rect in region["rects"]:
|
|
227
|
-
typed_rects.append(
|
|
244
|
+
typed_rects.append(dom_rectangle_from_dict(rect))
|
|
228
245
|
|
|
229
246
|
return InteractiveRegion(
|
|
230
247
|
tag_name=_get_str(region, "tag_name"),
|
|
@@ -235,7 +252,8 @@ def interactiveregion_from_dict(region: Dict[str, Any]) -> InteractiveRegion:
|
|
|
235
252
|
)
|
|
236
253
|
|
|
237
254
|
|
|
238
|
-
def
|
|
255
|
+
def visual_viewport_from_dict(viewport: Dict[str, Any]) -> VisualViewport:
|
|
256
|
+
r"""Create a :class:`VisualViewport` object from a dictionary."""
|
|
239
257
|
return VisualViewport(
|
|
240
258
|
height=_get_number(viewport, "height"),
|
|
241
259
|
width=_get_number(viewport, "width"),
|
|
@@ -252,7 +270,7 @@ def visualviewport_from_dict(viewport: Dict[str, Any]) -> VisualViewport:
|
|
|
252
270
|
|
|
253
271
|
|
|
254
272
|
def add_set_of_mark(
|
|
255
|
-
screenshot: bytes
|
|
273
|
+
screenshot: Union[bytes, Image.Image, io.BufferedIOBase],
|
|
256
274
|
ROIs: Dict[str, InteractiveRegion],
|
|
257
275
|
) -> Tuple[Image.Image, List[str], List[str], List[str]]:
|
|
258
276
|
if isinstance(screenshot, Image.Image):
|
|
@@ -272,6 +290,18 @@ def add_set_of_mark(
|
|
|
272
290
|
def _add_set_of_mark(
|
|
273
291
|
screenshot: Image.Image, ROIs: Dict[str, InteractiveRegion]
|
|
274
292
|
) -> Tuple[Image.Image, List[str], List[str], List[str]]:
|
|
293
|
+
r"""Add a set of marks to the screenshot.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
screenshot (Image.Image): The screenshot to add marks to.
|
|
297
|
+
ROIs (Dict[str, InteractiveRegion]): The regions to add marks to.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple[Image.Image, List[str], List[str], List[str]]: A tuple
|
|
301
|
+
containing the screenshot with marked ROIs, ROIs fully within the
|
|
302
|
+
images, ROIs located above the visible area, and ROIs located below
|
|
303
|
+
the visible area.
|
|
304
|
+
"""
|
|
275
305
|
visible_rects: List[str] = list()
|
|
276
306
|
rects_above: List[str] = list() # Scroll up to see
|
|
277
307
|
rects_below: List[str] = list() # Scroll down to see
|
|
@@ -284,22 +314,22 @@ def _add_set_of_mark(
|
|
|
284
314
|
for r in ROIs:
|
|
285
315
|
for rect in ROIs[r]["rects"]:
|
|
286
316
|
# Empty rectangles
|
|
287
|
-
if not rect:
|
|
288
|
-
continue
|
|
289
|
-
if rect["width"] * rect["height"] == 0:
|
|
317
|
+
if not rect or rect["width"] == 0 or rect["height"] == 0:
|
|
290
318
|
continue
|
|
291
319
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
# TODO: add scroll left and right?
|
|
321
|
+
horizontal_center = (rect["right"] + rect["left"]) / 2.0
|
|
322
|
+
vertical_center = (rect["top"] + rect["bottom"]) / 2.0
|
|
323
|
+
is_within_horizon = 0 <= horizontal_center < base.size[0]
|
|
324
|
+
is_above_viewport = vertical_center < 0
|
|
325
|
+
is_below_viewport = vertical_center >= base.size[1]
|
|
296
326
|
|
|
297
|
-
if
|
|
298
|
-
if
|
|
327
|
+
if is_within_horizon:
|
|
328
|
+
if is_above_viewport:
|
|
299
329
|
rects_above.append(r)
|
|
300
|
-
elif
|
|
330
|
+
elif is_below_viewport:
|
|
301
331
|
rects_below.append(r)
|
|
302
|
-
else:
|
|
332
|
+
else: # Fully visible
|
|
303
333
|
visible_rects.append(r)
|
|
304
334
|
_draw_roi(draw, int(r), fnt, rect)
|
|
305
335
|
|
|
@@ -314,9 +344,16 @@ def _draw_roi(
|
|
|
314
344
|
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
|
|
315
345
|
rect: DOMRectangle,
|
|
316
346
|
) -> None:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
347
|
+
r"""Draw a ROI on the image.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
draw (ImageDraw.ImageDraw): The draw object.
|
|
351
|
+
idx (int): The index of the ROI.
|
|
352
|
+
font (ImageFont.FreeTypeFont | ImageFont.ImageFont): The font.
|
|
353
|
+
rect (DOMRectangle): The DOM rectangle.
|
|
354
|
+
"""
|
|
355
|
+
color = _get_random_color(idx)
|
|
356
|
+
text_color = _get_text_color(color)
|
|
320
357
|
|
|
321
358
|
roi = ((rect["left"], rect["top"]), (rect["right"], rect["bottom"]))
|
|
322
359
|
|
|
@@ -351,9 +388,36 @@ def _draw_roi(
|
|
|
351
388
|
)
|
|
352
389
|
|
|
353
390
|
|
|
354
|
-
def
|
|
391
|
+
def _get_text_color(
|
|
392
|
+
bg_color: Tuple[int, int, int, int],
|
|
393
|
+
) -> Tuple[int, int, int, int]:
|
|
394
|
+
r"""Determine the ideal text color (black or white) for contrast.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
bg_color: The background color (R, G, B, A).
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
A tuple representing black or white color for text.
|
|
401
|
+
"""
|
|
402
|
+
luminance = bg_color[0] * 0.3 + bg_color[1] * 0.59 + bg_color[2] * 0.11
|
|
403
|
+
return (0, 0, 0, 255) if luminance > 120 else (255, 255, 255, 255)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _get_random_color(identifier: int) -> Tuple[int, int, int, int]:
|
|
407
|
+
r"""Generate a consistent random RGBA color based on the identifier.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
identifier: The ID used as a seed to ensure color consistency.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
A tuple representing (R, G, B, A) values.
|
|
414
|
+
"""
|
|
355
415
|
rnd = random.Random(int(identifier))
|
|
356
|
-
|
|
416
|
+
r = rnd.randint(0, 255)
|
|
417
|
+
g = rnd.randint(125, 255)
|
|
418
|
+
b = rnd.randint(0, 50)
|
|
419
|
+
color = [r, g, b]
|
|
420
|
+
# TODO: check why shuffle is needed?
|
|
357
421
|
rnd.shuffle(color)
|
|
358
422
|
color.append(255)
|
|
359
423
|
return cast(Tuple[int, int, int, int], tuple(color))
|
|
@@ -379,13 +443,11 @@ class BaseBrowser:
|
|
|
379
443
|
self.playwright = sync_playwright().start()
|
|
380
444
|
self.page_history: list = [] # stores the history of visited pages
|
|
381
445
|
|
|
382
|
-
#
|
|
383
|
-
self.cache_dir = "tmp/"
|
|
446
|
+
# Set the cache directory
|
|
447
|
+
self.cache_dir = "tmp/" if cache_dir is None else cache_dir
|
|
384
448
|
os.makedirs(self.cache_dir, exist_ok=True)
|
|
385
|
-
if cache_dir is not None:
|
|
386
|
-
self.cache_dir = cache_dir
|
|
387
449
|
|
|
388
|
-
#
|
|
450
|
+
# Load the page script
|
|
389
451
|
abs_dir_path = os.path.dirname(os.path.abspath(__file__))
|
|
390
452
|
page_script_path = os.path.join(abs_dir_path, "page_script.js")
|
|
391
453
|
|
|
@@ -398,34 +460,35 @@ class BaseBrowser:
|
|
|
398
460
|
f"Page script file not found at path: {page_script_path}"
|
|
399
461
|
)
|
|
400
462
|
|
|
401
|
-
def init(self):
|
|
463
|
+
def init(self) -> None:
|
|
402
464
|
r"""Initialize the browser."""
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
self.context = self.browser.new_context(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
r"""delete the cache directory and its contents."""
|
|
465
|
+
# Launch the browser, if headless is False, the browser will display
|
|
466
|
+
self.browser = self.playwright.chromium.launch(headless=self.headless)
|
|
467
|
+
# Create a new context
|
|
468
|
+
self.context = self.browser.new_context(accept_downloads=True)
|
|
469
|
+
# Create a new page
|
|
470
|
+
self.page = self.context.new_page()
|
|
471
|
+
|
|
472
|
+
def clean_cache(self) -> None:
|
|
473
|
+
r"""Delete the cache directory and its contents."""
|
|
413
474
|
if os.path.exists(self.cache_dir):
|
|
414
475
|
shutil.rmtree(self.cache_dir)
|
|
415
476
|
|
|
416
|
-
def _wait_for_load(self, timeout: int = 20):
|
|
477
|
+
def _wait_for_load(self, timeout: int = 20) -> None:
|
|
417
478
|
r"""Wait for a certain amount of time for the page to load."""
|
|
418
479
|
timeout_ms = timeout * 1000
|
|
419
480
|
|
|
420
481
|
self.page.wait_for_load_state("load", timeout=timeout_ms)
|
|
482
|
+
|
|
483
|
+
# TODO: check if this is needed
|
|
421
484
|
time.sleep(2)
|
|
422
485
|
|
|
423
|
-
def click_blank_area(self):
|
|
486
|
+
def click_blank_area(self) -> None:
|
|
424
487
|
r"""Click a blank area of the page to unfocus the current element."""
|
|
425
488
|
self.page.mouse.click(0, 0)
|
|
426
489
|
self._wait_for_load()
|
|
427
490
|
|
|
428
|
-
def visit_page(self, url: str):
|
|
491
|
+
def visit_page(self, url: str) -> None:
|
|
429
492
|
r"""Visit a page with the given URL."""
|
|
430
493
|
|
|
431
494
|
self.page.goto(url)
|
|
@@ -433,8 +496,8 @@ class BaseBrowser:
|
|
|
433
496
|
self.page_url = url
|
|
434
497
|
|
|
435
498
|
def ask_question_about_video(self, question: str) -> str:
|
|
436
|
-
r"""Ask a question about the video on the current page
|
|
437
|
-
|
|
499
|
+
r"""Ask a question about the video on the current page,
|
|
500
|
+
such as YouTube video.
|
|
438
501
|
|
|
439
502
|
Args:
|
|
440
503
|
question (str): The question to ask.
|
|
@@ -459,8 +522,9 @@ class BaseBrowser:
|
|
|
459
522
|
directory.
|
|
460
523
|
|
|
461
524
|
Returns:
|
|
462
|
-
Tuple[Image.Image, str]: A tuple containing the screenshot
|
|
463
|
-
|
|
525
|
+
Tuple[Image.Image, str]: A tuple containing the screenshot
|
|
526
|
+
image and the path to the image file if saved, otherwise
|
|
527
|
+
:obj:`None`.
|
|
464
528
|
"""
|
|
465
529
|
|
|
466
530
|
image_data = self.page.screenshot(timeout=60000)
|
|
@@ -468,12 +532,13 @@ class BaseBrowser:
|
|
|
468
532
|
|
|
469
533
|
file_path = None
|
|
470
534
|
if save_image:
|
|
471
|
-
#
|
|
535
|
+
# Get url name to form a file name
|
|
536
|
+
# TODO: Use a safer way for the url name
|
|
472
537
|
url_name = self.page_url.split("/")[-1]
|
|
473
538
|
for char in ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '.']:
|
|
474
539
|
url_name = url_name.replace(char, "_")
|
|
475
540
|
|
|
476
|
-
#
|
|
541
|
+
# Get formatted time: mmddhhmmss
|
|
477
542
|
timestamp = datetime.datetime.now().strftime("%m%d%H%M%S")
|
|
478
543
|
file_path = os.path.join(
|
|
479
544
|
self.cache_dir, f"{url_name}_{timestamp}.png"
|
|
@@ -492,23 +557,18 @@ class BaseBrowser:
|
|
|
492
557
|
|
|
493
558
|
Args:
|
|
494
559
|
scroll_ratio (float): The ratio of viewport height to scroll each
|
|
495
|
-
step (default: 0.
|
|
560
|
+
step (default: 0.8).
|
|
496
561
|
|
|
497
562
|
Returns:
|
|
498
563
|
List[str]: A list of paths to the screenshot files.
|
|
499
564
|
"""
|
|
500
565
|
screenshots = []
|
|
501
566
|
scroll_height = self.page.evaluate("document.body.scrollHeight")
|
|
567
|
+
assert self.page.viewport_size is not None
|
|
502
568
|
viewport_height = self.page.viewport_size["height"]
|
|
503
569
|
current_scroll = 0
|
|
504
570
|
screenshot_index = 1
|
|
505
571
|
|
|
506
|
-
url_name = self.page.url.split("/")[-1].replace(".", "_")
|
|
507
|
-
timestamp = datetime.datetime.now().strftime("%m%d%H%M%S")
|
|
508
|
-
base_file_path = os.path.join(
|
|
509
|
-
self.cache_dir, f"{url_name}_{timestamp}"
|
|
510
|
-
)
|
|
511
|
-
|
|
512
572
|
max_height = scroll_height - viewport_height
|
|
513
573
|
scroll_step = int(viewport_height * scroll_ratio)
|
|
514
574
|
|
|
@@ -520,14 +580,15 @@ class BaseBrowser:
|
|
|
520
580
|
f"{max_height}, step: {scroll_step}"
|
|
521
581
|
)
|
|
522
582
|
|
|
523
|
-
file_path = f"{base_file_path}_{screenshot_index}.png"
|
|
524
583
|
_, file_path = self.get_screenshot(save_image=True)
|
|
525
584
|
screenshots.append(file_path)
|
|
526
585
|
|
|
527
586
|
self.page.evaluate(f"window.scrollBy(0, {scroll_step})")
|
|
587
|
+
# Allow time for content to load
|
|
528
588
|
time.sleep(0.5)
|
|
529
589
|
|
|
530
590
|
current_scroll = self.page.evaluate("window.scrollY")
|
|
591
|
+
# Break if there is no significant scroll
|
|
531
592
|
if abs(current_scroll - last_height) < viewport_height * 0.1:
|
|
532
593
|
break
|
|
533
594
|
|
|
@@ -547,12 +608,16 @@ class BaseBrowser:
|
|
|
547
608
|
except Exception as e:
|
|
548
609
|
logger.warning(f"Error evaluating page script: {e}")
|
|
549
610
|
|
|
550
|
-
return
|
|
611
|
+
return visual_viewport_from_dict(
|
|
551
612
|
self.page.evaluate("MultimodalWebSurfer.getVisualViewport();")
|
|
552
613
|
)
|
|
553
614
|
|
|
554
|
-
def get_interactive_elements(self) ->
|
|
555
|
-
|
|
615
|
+
def get_interactive_elements(self) -> Dict[str, InteractiveRegion]:
|
|
616
|
+
r"""Get the interactive elements of the current page.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Dict[str, InteractiveRegion]: A dictionary of interactive elements.
|
|
620
|
+
"""
|
|
556
621
|
try:
|
|
557
622
|
self.page.evaluate(self.page_script)
|
|
558
623
|
except Exception as e:
|
|
@@ -565,12 +630,13 @@ class BaseBrowser:
|
|
|
565
630
|
|
|
566
631
|
typed_results: Dict[str, InteractiveRegion] = {}
|
|
567
632
|
for k in result:
|
|
568
|
-
typed_results[k] =
|
|
633
|
+
typed_results[k] = interactive_region_from_dict(result[k])
|
|
569
634
|
|
|
570
635
|
return typed_results # type: ignore[return-value]
|
|
571
636
|
|
|
572
637
|
def get_som_screenshot(
|
|
573
|
-
self,
|
|
638
|
+
self,
|
|
639
|
+
save_image: bool = False,
|
|
574
640
|
) -> Tuple[Image.Image, Union[str, None]]:
|
|
575
641
|
r"""Get a screenshot of the current viewport with interactive elements
|
|
576
642
|
marked.
|
|
@@ -608,15 +674,19 @@ class BaseBrowser:
|
|
|
608
674
|
return comp, file_path
|
|
609
675
|
|
|
610
676
|
def scroll_up(self) -> None:
|
|
677
|
+
r"""Scroll up the page."""
|
|
611
678
|
self.page.keyboard.press("PageUp")
|
|
612
679
|
|
|
613
680
|
def scroll_down(self) -> None:
|
|
681
|
+
r"""Scroll down the page."""
|
|
614
682
|
self.page.keyboard.press("PageDown")
|
|
615
683
|
|
|
616
684
|
def get_url(self) -> str:
|
|
685
|
+
r"""Get the URL of the current page."""
|
|
617
686
|
return self.page.url
|
|
618
687
|
|
|
619
|
-
def click_id(self, identifier: Union[str, int]):
|
|
688
|
+
def click_id(self, identifier: Union[str, int]) -> None:
|
|
689
|
+
r"""Click an element with the given identifier."""
|
|
620
690
|
if isinstance(identifier, int):
|
|
621
691
|
identifier = str(identifier)
|
|
622
692
|
target = self.page.locator(f"[__elementId='{identifier}']")
|
|
@@ -649,7 +719,7 @@ class BaseBrowser:
|
|
|
649
719
|
|
|
650
720
|
self._wait_for_load()
|
|
651
721
|
|
|
652
|
-
def extract_url_content(self):
|
|
722
|
+
def extract_url_content(self) -> str:
|
|
653
723
|
r"""Extract the content of the current page."""
|
|
654
724
|
content = self.page.content()
|
|
655
725
|
return content
|
|
@@ -821,7 +891,6 @@ class BaseBrowser:
|
|
|
821
891
|
|
|
822
892
|
def close(self):
|
|
823
893
|
self.browser.close()
|
|
824
|
-
self.playwright.stop()
|
|
825
894
|
|
|
826
895
|
# ruff: noqa: E501
|
|
827
896
|
def show_interactive_elements(self):
|
|
@@ -846,7 +915,7 @@ class BaseBrowser:
|
|
|
846
915
|
return markdown_content
|
|
847
916
|
|
|
848
917
|
|
|
849
|
-
class
|
|
918
|
+
class BrowserToolkit(BaseToolkit):
|
|
850
919
|
r"""A class for browsing the web and interacting with web pages.
|
|
851
920
|
|
|
852
921
|
This class provides methods for browsing the web and interacting with web
|
|
@@ -862,7 +931,7 @@ class WebToolkit(BaseToolkit):
|
|
|
862
931
|
planning_agent_model: Optional[BaseModelBackend] = None,
|
|
863
932
|
output_language: str = "en",
|
|
864
933
|
):
|
|
865
|
-
r"""Initialize the
|
|
934
|
+
r"""Initialize the BrowserToolkit instance.
|
|
866
935
|
|
|
867
936
|
Args:
|
|
868
937
|
headless (bool): Whether to run the browser in headless mode.
|
|
@@ -1026,9 +1095,7 @@ out the information you need. Sometimes they are extremely useful.
|
|
|
1026
1095
|
"""
|
|
1027
1096
|
|
|
1028
1097
|
# get current state
|
|
1029
|
-
som_screenshot,
|
|
1030
|
-
save_image=True
|
|
1031
|
-
)
|
|
1098
|
+
som_screenshot, _ = self.browser.get_som_screenshot(save_image=True)
|
|
1032
1099
|
img = _reload_image(som_screenshot)
|
|
1033
1100
|
message = BaseMessage.make_user_message(
|
|
1034
1101
|
role_name='user', content=observe_prompt, image_list=[img]
|
|
@@ -1222,7 +1289,7 @@ Your output should be in json format, including the following fields:
|
|
|
1222
1289
|
return False, replanned_schema
|
|
1223
1290
|
|
|
1224
1291
|
@dependencies_required("playwright")
|
|
1225
|
-
def
|
|
1292
|
+
def browse_url(
|
|
1226
1293
|
self, task_prompt: str, start_url: str, round_limit: int = 12
|
|
1227
1294
|
) -> str:
|
|
1228
1295
|
r"""A powerful toolkit which can simulate the browser interaction to solve the task which needs multi-step actions.
|
|
@@ -1303,4 +1370,4 @@ Your output should be in json format, including the following fields:
|
|
|
1303
1370
|
return simulation_result
|
|
1304
1371
|
|
|
1305
1372
|
def get_tools(self) -> List[FunctionTool]:
|
|
1306
|
-
return [FunctionTool(self.
|
|
1373
|
+
return [FunctionTool(self.browse_url)]
|
camel/toolkits/search_toolkit.py
CHANGED
|
@@ -704,6 +704,68 @@ class SearchToolkit(BaseToolkit):
|
|
|
704
704
|
except Exception as e:
|
|
705
705
|
return [{"error": f"An unexpected error occurred: {e!s}"}]
|
|
706
706
|
|
|
707
|
+
@api_keys_required([(None, 'BOCHA_API_KEY')])
|
|
708
|
+
def search_bocha(
|
|
709
|
+
self,
|
|
710
|
+
query: str,
|
|
711
|
+
freshness: str = "noLimit",
|
|
712
|
+
summary: bool = False,
|
|
713
|
+
count: int = 10,
|
|
714
|
+
page: int = 1,
|
|
715
|
+
) -> Dict[str, Any]:
|
|
716
|
+
r"""Query the Bocha AI search API and return search results.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
query (str): The search query.
|
|
720
|
+
freshness (str): Time frame filter for search results. Default
|
|
721
|
+
is "noLimit". Options include:
|
|
722
|
+
- 'noLimit': no limit (default).
|
|
723
|
+
- 'oneDay': past day.
|
|
724
|
+
- 'oneWeek': past week.
|
|
725
|
+
- 'oneMonth': past month.
|
|
726
|
+
- 'oneYear': past year.
|
|
727
|
+
summary (bool): Whether to include text summaries in results.
|
|
728
|
+
Default is False.
|
|
729
|
+
count (int): Number of results to return (1-50). Default is 10.
|
|
730
|
+
page (int): Page number of results. Default is 1.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Dict[str, Any]: A dictionary containing search results, including
|
|
734
|
+
web pages, images, and videos if available. The structure
|
|
735
|
+
follows the Bocha AI search API response format.
|
|
736
|
+
"""
|
|
737
|
+
import json
|
|
738
|
+
|
|
739
|
+
BOCHA_API_KEY = os.getenv("BOCHA_API_KEY")
|
|
740
|
+
|
|
741
|
+
url = "https://api.bochaai.com/v1/web-search"
|
|
742
|
+
headers = {
|
|
743
|
+
"Authorization": f"Bearer {BOCHA_API_KEY}",
|
|
744
|
+
"Content-Type": "application/json",
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
payload = json.dumps(
|
|
748
|
+
{
|
|
749
|
+
"query": query,
|
|
750
|
+
"freshness": freshness,
|
|
751
|
+
"summary": summary,
|
|
752
|
+
"count": count,
|
|
753
|
+
"page": page,
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
try:
|
|
757
|
+
response = requests.post(url, headers=headers, data=payload)
|
|
758
|
+
if response.status_code != 200:
|
|
759
|
+
return {
|
|
760
|
+
"error": (
|
|
761
|
+
f"Bocha API failed with {response.status_code}: "
|
|
762
|
+
f"{response.text}"
|
|
763
|
+
)
|
|
764
|
+
}
|
|
765
|
+
return response.json()["data"]
|
|
766
|
+
except requests.exceptions.RequestException as e:
|
|
767
|
+
return {"error": f"Bocha AI search failed: {e!s}"}
|
|
768
|
+
|
|
707
769
|
def get_tools(self) -> List[FunctionTool]:
|
|
708
770
|
r"""Returns a list of FunctionTool objects representing the
|
|
709
771
|
functions in the toolkit.
|
|
@@ -720,4 +782,5 @@ class SearchToolkit(BaseToolkit):
|
|
|
720
782
|
FunctionTool(self.query_wolfram_alpha),
|
|
721
783
|
FunctionTool(self.tavily_search),
|
|
722
784
|
FunctionTool(self.search_brave),
|
|
785
|
+
FunctionTool(self.search_bocha),
|
|
723
786
|
]
|
camel/types/enums.py
CHANGED
|
@@ -249,6 +249,7 @@ class ModelType(UnifiedModelType, Enum):
|
|
|
249
249
|
self.is_siliconflow,
|
|
250
250
|
self.is_zhipuai,
|
|
251
251
|
self.is_aiml,
|
|
252
|
+
self.is_azure_openai,
|
|
252
253
|
]
|
|
253
254
|
)
|
|
254
255
|
|
|
@@ -889,6 +890,7 @@ class ModelPlatformType(Enum):
|
|
|
889
890
|
MOONSHOT = "moonshot"
|
|
890
891
|
SILICONFLOW = "siliconflow"
|
|
891
892
|
AIML = "aiml"
|
|
893
|
+
VOLCANO = "volcano"
|
|
892
894
|
|
|
893
895
|
@property
|
|
894
896
|
def is_openai(self) -> bool:
|
|
@@ -1011,6 +1013,11 @@ class ModelPlatformType(Enum):
|
|
|
1011
1013
|
r"""Returns whether this platform is AIML."""
|
|
1012
1014
|
return self is ModelPlatformType.AIML
|
|
1013
1015
|
|
|
1016
|
+
@property
|
|
1017
|
+
def is_volcano(self) -> bool:
|
|
1018
|
+
r"""Returns whether this platform is volcano."""
|
|
1019
|
+
return self is ModelPlatformType.VOLCANO
|
|
1020
|
+
|
|
1014
1021
|
|
|
1015
1022
|
class AudioModelType(Enum):
|
|
1016
1023
|
TTS_1 = "tts-1"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
camel/__init__.py,sha256=
|
|
1
|
+
camel/__init__.py,sha256=6XPf3TQFxgCHXxPaRb-Lda_NJ-gHT1IsrHIeYeMzGGA,912
|
|
2
2
|
camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
|
|
3
3
|
camel/human.py,sha256=9X09UmxI2JqQnhrFfnZ3B9EzFmVfdSWQcjLWTIXKXe0,4962
|
|
4
4
|
camel/logger.py,sha256=j6mPsLJyKOn16o6Um57882mHsURQ8h-jia6Jd_34wRA,4239
|
|
@@ -134,11 +134,11 @@ camel/messages/conversion/sharegpt/__init__.py,sha256=oWUuHV5w85kxqhz_hoElLmCfzL
|
|
|
134
134
|
camel/messages/conversion/sharegpt/function_call_formatter.py,sha256=cn7e7CfmxEVFlfOqhjhNuA8nuWvWD6hXYn-3okXNxxQ,1832
|
|
135
135
|
camel/messages/conversion/sharegpt/hermes/__init__.py,sha256=mxuMSm-neaTgInIjYXuIVdC310E6jKJzM3IdtaJ4qY4,812
|
|
136
136
|
camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py,sha256=-9TT8iOQ-ieKSKR_PmJSA5Bi0uBx-qR7WQ6vxuFkorM,4639
|
|
137
|
-
camel/models/__init__.py,sha256=
|
|
137
|
+
camel/models/__init__.py,sha256=iNZ1LcesJ3YaAbNRxHTA-sUaxfjn6gUQ-EQArll21KQ,2769
|
|
138
138
|
camel/models/_utils.py,sha256=hob1ehnS5xZitMCdYToHVgaTB55JnaP4_DSWnTEfVsg,2045
|
|
139
139
|
camel/models/aiml_model.py,sha256=4FW66DxmVMPWAJckh4UjMM6eD1QNyrAPAPtrpmWxzjc,6524
|
|
140
140
|
camel/models/anthropic_model.py,sha256=8XAj9sVaN1X0hvrL9a-qsmkAFWoGe1Ozj5XZsXYe1UI,5894
|
|
141
|
-
camel/models/azure_openai_model.py,sha256=
|
|
141
|
+
camel/models/azure_openai_model.py,sha256=AblW2scYp12_odI1GG0ATHI8-Tn7d6SCsxHe7g66rWs,10386
|
|
142
142
|
camel/models/base_audio_model.py,sha256=QkLqh0v-5kcE_jwFB5xAgvztAqB2Bot4_iG9sZdcl8A,2986
|
|
143
143
|
camel/models/base_model.py,sha256=RolL8fRwVpfz8g9lpb_71h0mYTNl-U63f8KBy6hc3E0,10679
|
|
144
144
|
camel/models/cohere_model.py,sha256=RAYHCyppDQxQ7BOR-e314AagB09vRxoScoHc-FtL6Bc,13355
|
|
@@ -149,14 +149,14 @@ camel/models/groq_model.py,sha256=FgXOHmIKAxGFASUmmk5tK49bPcXsr7diB3zRFGg9XDA,73
|
|
|
149
149
|
camel/models/internlm_model.py,sha256=4nr5LXhxBfOjm-0i65pXyaS0_sT5oAXKXaUfkijAGmQ,5612
|
|
150
150
|
camel/models/litellm_model.py,sha256=xi4kDd0FKuznKtox8ArsB39u40ueOhcb-CpWv4bcbXw,5544
|
|
151
151
|
camel/models/mistral_model.py,sha256=OB948fRVnXikVIDO3PqxV0zb_qpwwta0DIW1bbX3SYI,11666
|
|
152
|
-
camel/models/model_factory.py,sha256=
|
|
152
|
+
camel/models/model_factory.py,sha256=GdjGCxslqe04oWvpfiOdPl0ZGzuGooeA9M8ppby6hFo,6863
|
|
153
153
|
camel/models/model_manager.py,sha256=gfpL-WUxuTXgNeCkIVg8Y0zRvxMqRLX8JGt0XEAPQ8Y,9214
|
|
154
154
|
camel/models/moonshot_model.py,sha256=DNZzDYz0AWU1q6pIvbPALqesejoawwuKzeP0_ZbjDSg,6149
|
|
155
155
|
camel/models/nemotron_model.py,sha256=jJrW8tpTlEJDT1FjflB9krhgEQhD5KBeLmyUIcZvWPk,3886
|
|
156
156
|
camel/models/nvidia_model.py,sha256=lqp1iPwVDq6zSQ9B0SyBZ48Z3J5WbXwPshwlhj1ogZ8,6711
|
|
157
157
|
camel/models/ollama_model.py,sha256=byJ0YbMlilEFRKJZIot-MPUcojwMHLIaBES0a1SURtg,10604
|
|
158
158
|
camel/models/openai_audio_models.py,sha256=fYpxFvxT8p93KVb5BYODTuI5wdNXV9pu_bvxfARgVYk,13193
|
|
159
|
-
camel/models/openai_compatible_model.py,sha256=
|
|
159
|
+
camel/models/openai_compatible_model.py,sha256=Nmps04Fo0ILmynE8wbKVzTrQ0VDyOrDR1ICD1nGrjd0,8142
|
|
160
160
|
camel/models/openai_model.py,sha256=CbfD9yVtAltyqdFpjnLXncFnmaGPDZq8JhJDaSfG0pc,10186
|
|
161
161
|
camel/models/qwen_model.py,sha256=_LeeB0yrXRMI-gZOEEOHg0bWNOJpuQHf2G7u40--3r8,7064
|
|
162
162
|
camel/models/reka_model.py,sha256=15DscZf3lbqsIzm6kzjzDrhblBt1_0xlphT4isuQMu0,10146
|
|
@@ -166,6 +166,7 @@ camel/models/siliconflow_model.py,sha256=c5vk4zAhZVf8pDF1uh-iSa_v8d0QoPLuIN27Eem
|
|
|
166
166
|
camel/models/stub_model.py,sha256=dygYoxemnWWaxEX21L8QyKe-c75ti2CK9HnTuyHL5vs,5160
|
|
167
167
|
camel/models/togetherai_model.py,sha256=-YwZV1S1bkrX8jGguQI5dbtIHVuqhv96MoAcl33ptPo,6657
|
|
168
168
|
camel/models/vllm_model.py,sha256=dzH4rYr2Se7cejk2hobblaW-s483uxPxb8976RQE8x0,6884
|
|
169
|
+
camel/models/volcano_model.py,sha256=inYDiKOfGvq8o3XW4KVQIrXiZOhXQfB4HfCHGCWHPKs,3792
|
|
169
170
|
camel/models/yi_model.py,sha256=V4sc9n8MAKVfjGO-NU0I8W4lGKdORSCbMV020SHT3R0,6180
|
|
170
171
|
camel/models/zhipuai_model.py,sha256=o3uoTY30p1yUIklvoRMyr8JX39xZ5mLVKSTtUknW8nE,6517
|
|
171
172
|
camel/models/reward/__init__.py,sha256=MqPN6wXh7Y1SoeNoFlYaMG6xHzLG0CYsv_3kB2atIQk,984
|
|
@@ -255,11 +256,12 @@ camel/terminators/__init__.py,sha256=t8uqrkUnXEOYMXQDgaBkMFJ0EXFKI0kmx4cUimli3Ls
|
|
|
255
256
|
camel/terminators/base.py,sha256=xmJzERX7GdSXcxZjAHHODa0rOxRChMSRboDCNHWSscs,1511
|
|
256
257
|
camel/terminators/response_terminator.py,sha256=n3G5KP6Oj7-7WlRN0yFcrtLpqAJKaKS0bmhrWlFfCgQ,4982
|
|
257
258
|
camel/terminators/token_limit_terminator.py,sha256=YWv6ZR8R9yI2Qnf_3xES5bEE_O5bb2CxQ0EUXfMh34c,2118
|
|
258
|
-
camel/toolkits/__init__.py,sha256=
|
|
259
|
+
camel/toolkits/__init__.py,sha256=SJ1Agk9YI_qH4gKX-pcvhl-m2t0Nori6R9Fs0gXReDs,3699
|
|
259
260
|
camel/toolkits/arxiv_toolkit.py,sha256=d0Zn8LQGENhtlZ0BHlDr1pUV8xHOc6TOenAaKgbelu8,6279
|
|
260
261
|
camel/toolkits/ask_news_toolkit.py,sha256=PAxio8I2eTau9TgOu1jyFC9fsHhvGb-aLIkroWPtwx4,23268
|
|
261
262
|
camel/toolkits/audio_analysis_toolkit.py,sha256=LC0C6SEIwko8HqkT-C3ub6Ila2PfuIbKLBOEjrrF6BE,8552
|
|
262
263
|
camel/toolkits/base.py,sha256=7WRovKrz380b25lYdwT-2FCXzS3dkllOjT53hmmCg_I,1999
|
|
264
|
+
camel/toolkits/browser_toolkit.py,sha256=Pbw7CoiHPtbFQagmXvCUiVHihCXwaN6uUbwCAfC-jso,51016
|
|
263
265
|
camel/toolkits/code_execution.py,sha256=seqTtjulBZXH4qd5m2YAXQaxyL2_n2ekmqsYB-wBxvw,4547
|
|
264
266
|
camel/toolkits/dalle_toolkit.py,sha256=Usmw3JiJErLQgWSB1qKq_bOACNwbUTQPFc_EsVzTrGo,5115
|
|
265
267
|
camel/toolkits/dappier_toolkit.py,sha256=_69IAmXE2QSbwGxnSEycaV2XrrkiM5wKI6heM7-4MfU,8175
|
|
@@ -285,7 +287,7 @@ camel/toolkits/page_script.js,sha256=gypbuQ_gn_oa3rQDoCN_q-kJ0jND1eSvY-30PufPZmQ
|
|
|
285
287
|
camel/toolkits/pubmed_toolkit.py,sha256=vrd5GIhSYt9Z8EHaWkFb0x9i6_TP7pQZc7jlLHSol04,12180
|
|
286
288
|
camel/toolkits/reddit_toolkit.py,sha256=cTqEq1CRaLq9XxUHkHCmd09tRzb5Mz_bUs2JV58ewrs,8012
|
|
287
289
|
camel/toolkits/retrieval_toolkit.py,sha256=y_mQtknrSIDDXSyQb-4FY6ahV_mOxkBhDkA2eMIVnz0,3801
|
|
288
|
-
camel/toolkits/search_toolkit.py,sha256=
|
|
290
|
+
camel/toolkits/search_toolkit.py,sha256=KxJMB8tz37uyqlzHSK9X28hSZZMK793aOwkYT8jBnGc,32131
|
|
289
291
|
camel/toolkits/semantic_scholar_toolkit.py,sha256=Kp-5rz99rkeUsUmk5ShQdNKJRGVs78hQvCNg-NQMFDk,11547
|
|
290
292
|
camel/toolkits/slack_toolkit.py,sha256=n8cn3kZIc27B-2KMTRK6Nsdan37SwMqBiBi1PMtuUvQ,10744
|
|
291
293
|
camel/toolkits/stripe_toolkit.py,sha256=1sCywkpo8mh4E_KwxFKLhAb-G5uZ47NXXQvcddThjqg,9781
|
|
@@ -295,7 +297,6 @@ camel/toolkits/twitter_toolkit.py,sha256=a2OLSJSW2wY7pOwOApb1qchZPXzH22Rbgm9Yd7-
|
|
|
295
297
|
camel/toolkits/video_analysis_toolkit.py,sha256=lDAY6YP1zXykSxt8Qanf0WZR3l1p8c4akKPkaF5R3wU,15064
|
|
296
298
|
camel/toolkits/video_download_toolkit.py,sha256=XjZICJTOG4dmKxfkHxYxmBMFESsOX51GvTeQXAQslMU,7104
|
|
297
299
|
camel/toolkits/weather_toolkit.py,sha256=qHAMD56zqd5GWnEWiaA_0aBDwvgacdx0pAHScinY4GY,6965
|
|
298
|
-
camel/toolkits/web_toolkit.py,sha256=DapiYYP95EFMVB1hU38a052nUPs4L5WdE25o_74h1Cw,48407
|
|
299
300
|
camel/toolkits/whatsapp_toolkit.py,sha256=MBY92WLLptkXxnl4Ky5erGwI415XLFFGu9i2q6_b0oQ,5736
|
|
300
301
|
camel/toolkits/zapier_toolkit.py,sha256=mgYxRGPw7_VZVj0xU9XwHewqFcV49eUjvuum_IHfuNs,6854
|
|
301
302
|
camel/toolkits/open_api_specs/security_config.py,sha256=ZVnBa_zEifaE_ao2xsvV5majuJHpn2Tn7feMDOnj-eo,898
|
|
@@ -324,7 +325,7 @@ camel/toolkits/open_api_specs/web_scraper/openapi.yaml,sha256=u_WalQ01e8W1D27VnZ
|
|
|
324
325
|
camel/toolkits/open_api_specs/web_scraper/paths/__init__.py,sha256=OKCZrQCDwaWtXIN_2rA9FSqEvgpQRieRoHh7Ek6N16A,702
|
|
325
326
|
camel/toolkits/open_api_specs/web_scraper/paths/scraper.py,sha256=aWy1_ppV4NVVEZfnbN3tu9XA9yAPAC9bRStJ5JuXMRU,1117
|
|
326
327
|
camel/types/__init__.py,sha256=VLWhAt857IFct3XepY5BNOIhyhDhfmODTezr9jhO_TI,2251
|
|
327
|
-
camel/types/enums.py,sha256=
|
|
328
|
+
camel/types/enums.py,sha256=b3KZqTbenkWSxC7QrgR_rYO1bV8tpcmTczwofOiByJA,34343
|
|
328
329
|
camel/types/openai_types.py,sha256=8ZFzLe-zGmKNPfuVZFzxlxAX98lGf18gtrPhOgMmzus,2104
|
|
329
330
|
camel/types/unified_model_type.py,sha256=GP5GYtA3RfvLsqnk1c4UcOaRKMFhjDgZrLr0ln6JFw8,4253
|
|
330
331
|
camel/types/agents/__init__.py,sha256=cbvVkogPoZgcwZrgxLH6EtpGXk0kavF79nOic0Dc1vg,786
|
|
@@ -340,7 +341,7 @@ camel/verifiers/__init__.py,sha256=p6UEyvaOlwUQaFACGB4C07fL1xSnpTouElt19YRuneQ,9
|
|
|
340
341
|
camel/verifiers/base.py,sha256=efWZV9g58IHzJ24U4zr109y34CaAi8tV9WZPMCzP3YI,12017
|
|
341
342
|
camel/verifiers/models.py,sha256=hC6m_YxEX-mqi_tkCNZHZWLBWf04ZTyv5vfKR-BEyU4,2818
|
|
342
343
|
camel/verifiers/python_verifier.py,sha256=bj-UGxeJTZzxVVa3a8IEQ1lNOpSaaW3JdGnUEoPeQD0,7519
|
|
343
|
-
camel_ai-0.2.
|
|
344
|
-
camel_ai-0.2.
|
|
345
|
-
camel_ai-0.2.
|
|
346
|
-
camel_ai-0.2.
|
|
344
|
+
camel_ai-0.2.27.dist-info/METADATA,sha256=B4jItSRHDPybZtfm-i0xxUq3z4HWUQ99OEdaRJ33Mfw,37992
|
|
345
|
+
camel_ai-0.2.27.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
346
|
+
camel_ai-0.2.27.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
|
|
347
|
+
camel_ai-0.2.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|