camel-ai 0.2.42__py3-none-any.whl → 0.2.44__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/configs/__init__.py +3 -0
- camel/configs/anthropic_config.py +2 -24
- camel/configs/ppio_config.py +102 -0
- camel/configs/reka_config.py +1 -7
- camel/configs/samba_config.py +1 -7
- camel/configs/togetherai_config.py +1 -7
- camel/datasets/few_shot_generator.py +1 -0
- camel/embeddings/__init__.py +4 -0
- camel/embeddings/azure_embedding.py +119 -0
- camel/embeddings/together_embedding.py +136 -0
- camel/environments/__init__.py +3 -0
- camel/environments/multi_step.py +12 -10
- camel/environments/single_step.py +14 -2
- camel/environments/tic_tac_toe.py +518 -0
- camel/extractors/python_strategies.py +14 -5
- camel/loaders/__init__.py +2 -0
- camel/loaders/crawl4ai_reader.py +230 -0
- camel/models/__init__.py +2 -0
- camel/models/azure_openai_model.py +10 -2
- camel/models/base_model.py +111 -28
- camel/models/cohere_model.py +5 -1
- camel/models/deepseek_model.py +4 -0
- camel/models/gemini_model.py +8 -2
- camel/models/model_factory.py +3 -0
- camel/models/ollama_model.py +8 -2
- camel/models/openai_compatible_model.py +8 -2
- camel/models/openai_model.py +16 -4
- camel/models/ppio_model.py +184 -0
- camel/models/togetherai_model.py +106 -31
- camel/models/vllm_model.py +140 -57
- camel/societies/workforce/workforce.py +26 -3
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/browser_toolkit.py +11 -3
- camel/toolkits/google_calendar_toolkit.py +432 -0
- camel/toolkits/search_toolkit.py +119 -1
- camel/types/enums.py +74 -3
- camel/types/unified_model_type.py +5 -0
- camel/verifiers/python_verifier.py +93 -9
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.44.dist-info}/METADATA +21 -2
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.44.dist-info}/RECORD +43 -36
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.44.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.44.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/__init__.py
CHANGED
|
@@ -35,6 +35,7 @@ from .google_maps_toolkit import GoogleMapsToolkit
|
|
|
35
35
|
from .code_execution import CodeExecutionToolkit
|
|
36
36
|
from .github_toolkit import GithubToolkit
|
|
37
37
|
from .google_scholar_toolkit import GoogleScholarToolkit
|
|
38
|
+
from .google_calendar_toolkit import GoogleCalendarToolkit
|
|
38
39
|
from .arxiv_toolkit import ArxivToolkit
|
|
39
40
|
from .slack_toolkit import SlackToolkit
|
|
40
41
|
from .whatsapp_toolkit import WhatsAppToolkit
|
|
@@ -91,6 +92,7 @@ __all__ = [
|
|
|
91
92
|
'AskNewsToolkit',
|
|
92
93
|
'AsyncAskNewsToolkit',
|
|
93
94
|
'GoogleScholarToolkit',
|
|
95
|
+
'GoogleCalendarToolkit',
|
|
94
96
|
'NotionToolkit',
|
|
95
97
|
'ArxivToolkit',
|
|
96
98
|
'HumanToolkit',
|
|
@@ -20,6 +20,7 @@ import random
|
|
|
20
20
|
import re
|
|
21
21
|
import shutil
|
|
22
22
|
import time
|
|
23
|
+
import urllib.parse
|
|
23
24
|
from copy import deepcopy
|
|
24
25
|
from typing import (
|
|
25
26
|
TYPE_CHECKING,
|
|
@@ -546,8 +547,10 @@ class BaseBrowser:
|
|
|
546
547
|
file_path = None
|
|
547
548
|
if save_image:
|
|
548
549
|
# Get url name to form a file name
|
|
549
|
-
#
|
|
550
|
-
|
|
550
|
+
# Use urlparser for a safer extraction the url name
|
|
551
|
+
parsed_url = urllib.parse.urlparse(self.page_url)
|
|
552
|
+
url_name = os.path.basename(str(parsed_url.path)) or "index"
|
|
553
|
+
|
|
551
554
|
for char in ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '.']:
|
|
552
555
|
url_name = url_name.replace(char, "_")
|
|
553
556
|
|
|
@@ -673,7 +676,8 @@ class BaseBrowser:
|
|
|
673
676
|
rects, # type: ignore[arg-type]
|
|
674
677
|
)
|
|
675
678
|
if save_image:
|
|
676
|
-
|
|
679
|
+
parsed_url = urllib.parse.urlparse(self.page_url)
|
|
680
|
+
url_name = os.path.basename(str(parsed_url.path)) or "index"
|
|
677
681
|
for char in ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '.']:
|
|
678
682
|
url_name = url_name.replace(char, "_")
|
|
679
683
|
timestamp = datetime.datetime.now().strftime("%m%d%H%M%S")
|
|
@@ -1171,6 +1175,8 @@ out the information you need. Sometimes they are extremely useful.
|
|
|
1171
1175
|
message = BaseMessage.make_user_message(
|
|
1172
1176
|
role_name='user', content=observe_prompt, image_list=[img]
|
|
1173
1177
|
)
|
|
1178
|
+
# Reset the history message of web_agent.
|
|
1179
|
+
self.web_agent.reset()
|
|
1174
1180
|
resp = self.web_agent.step(message)
|
|
1175
1181
|
|
|
1176
1182
|
resp_content = resp.msgs[0].content
|
|
@@ -1400,6 +1406,8 @@ Your output should be in json format, including the following fields:
|
|
|
1400
1406
|
- `if_need_replan`: bool, A boolean value indicating whether the task needs to be fundamentally replanned.
|
|
1401
1407
|
- `replanned_schema`: str, The replanned schema for the task, which should not be changed too much compared with the original one. If the task does not need to be replanned, the value should be an empty string.
|
|
1402
1408
|
"""
|
|
1409
|
+
# Reset the history message of planning_agent.
|
|
1410
|
+
self.planning_agent.reset()
|
|
1403
1411
|
resp = self.planning_agent.step(replanning_prompt)
|
|
1404
1412
|
resp_dict = _parse_json_output(resp.msgs[0].content)
|
|
1405
1413
|
|
|
@@ -0,0 +1,432 @@
|
|
|
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
|
+
# Setup guide - https://developers.google.com/calendar/api/quickstart/python
|
|
15
|
+
|
|
16
|
+
import datetime
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any, Dict, List, Optional, Union
|
|
19
|
+
|
|
20
|
+
from camel.logger import get_logger
|
|
21
|
+
from camel.toolkits import FunctionTool
|
|
22
|
+
from camel.toolkits.base import BaseToolkit
|
|
23
|
+
from camel.utils import MCPServer, api_keys_required
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@MCPServer()
|
|
31
|
+
class GoogleCalendarToolkit(BaseToolkit):
|
|
32
|
+
r"""A class representing a toolkit for Google Calendar operations.
|
|
33
|
+
|
|
34
|
+
This class provides methods for creating events, retrieving events,
|
|
35
|
+
updating events, and deleting events from a Google Calendar.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
timeout: Optional[float] = None,
|
|
41
|
+
):
|
|
42
|
+
r"""Initializes a new instance of the GoogleCalendarToolkit class.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
timeout (Optional[float]): The timeout value for API requests
|
|
46
|
+
in seconds. If None, no timeout is applied.
|
|
47
|
+
(default: :obj:`None`)
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(timeout=timeout)
|
|
50
|
+
self.service = self._get_calendar_service()
|
|
51
|
+
|
|
52
|
+
def create_event(
|
|
53
|
+
self,
|
|
54
|
+
event_title: str,
|
|
55
|
+
start_time: str,
|
|
56
|
+
end_time: str,
|
|
57
|
+
description: str = "",
|
|
58
|
+
location: str = "",
|
|
59
|
+
attendees_email: Optional[List[str]] = None,
|
|
60
|
+
timezone: str = "UTC",
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
r"""Creates an event in the user's primary Google Calendar.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
event_title (str): Title of the event.
|
|
66
|
+
start_time (str): Start time in ISO format (YYYY-MM-DDTHH:MM:SS).
|
|
67
|
+
end_time (str): End time in ISO format (YYYY-MM-DDTHH:MM:SS).
|
|
68
|
+
description (str, optional): Description of the event.
|
|
69
|
+
location (str, optional): Location of the event.
|
|
70
|
+
attendees_email (List[str], optional): List of email addresses.
|
|
71
|
+
(default: :obj:`None`)
|
|
72
|
+
timezone (str, optional): Timezone for the event.
|
|
73
|
+
(default: :obj:`UTC`)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
dict: A dictionary containing details of the created event.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If the event creation fails.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
# Handle ISO format with or without timezone info
|
|
83
|
+
if 'Z' in start_time or '+' in start_time:
|
|
84
|
+
datetime.datetime.fromisoformat(
|
|
85
|
+
start_time.replace('Z', '+00:00')
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S")
|
|
89
|
+
|
|
90
|
+
if 'Z' in end_time or '+' in end_time:
|
|
91
|
+
datetime.datetime.fromisoformat(
|
|
92
|
+
end_time.replace('Z', '+00:00')
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
datetime.datetime.strptime(end_time, "%Y-%m-%dT%H:%M:%S")
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
error_msg = f"Time format error: {e!s}. Expected ISO "
|
|
98
|
+
"format: YYYY-MM-DDTHH:MM:SS"
|
|
99
|
+
logger.error(error_msg)
|
|
100
|
+
return {"error": error_msg}
|
|
101
|
+
|
|
102
|
+
if attendees_email is None:
|
|
103
|
+
attendees_email = []
|
|
104
|
+
|
|
105
|
+
# Verify email addresses with improved validation
|
|
106
|
+
valid_emails = []
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
email_pattern = re.compile(
|
|
110
|
+
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
for email in attendees_email:
|
|
114
|
+
if email_pattern.match(email):
|
|
115
|
+
valid_emails.append(email)
|
|
116
|
+
else:
|
|
117
|
+
logger.error(f"Invalid email address: {email}")
|
|
118
|
+
return {"error": f"Invalid email address: {email}"}
|
|
119
|
+
|
|
120
|
+
event: Dict[str, Any] = {
|
|
121
|
+
'summary': event_title,
|
|
122
|
+
'location': location,
|
|
123
|
+
'description': description,
|
|
124
|
+
'start': {
|
|
125
|
+
'dateTime': start_time,
|
|
126
|
+
'timeZone': timezone,
|
|
127
|
+
},
|
|
128
|
+
'end': {
|
|
129
|
+
'dateTime': end_time,
|
|
130
|
+
'timeZone': timezone,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if valid_emails:
|
|
135
|
+
event['attendees'] = [{'email': email} for email in valid_emails]
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
created_event = (
|
|
139
|
+
self.service.events()
|
|
140
|
+
.insert(calendarId='primary', body=event)
|
|
141
|
+
.execute()
|
|
142
|
+
)
|
|
143
|
+
return {
|
|
144
|
+
'Event ID': created_event.get('id'),
|
|
145
|
+
'EventTitle': created_event.get('summary'),
|
|
146
|
+
'Start Time': created_event.get('start', {}).get('dateTime'),
|
|
147
|
+
'End Time': created_event.get('end', {}).get('dateTime'),
|
|
148
|
+
'Link': created_event.get('htmlLink'),
|
|
149
|
+
}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
error_msg = f"Failed to create event: {e!s}"
|
|
152
|
+
logger.error(error_msg)
|
|
153
|
+
return {"error": error_msg}
|
|
154
|
+
|
|
155
|
+
def get_events(
|
|
156
|
+
self, max_results: int = 10, time_min: Optional[str] = None
|
|
157
|
+
) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
158
|
+
r"""Retrieves upcoming events from the user's primary Google Calendar.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
max_results (int, optional): Maximum number of events to retrieve.
|
|
162
|
+
(default: :obj:`10`)
|
|
163
|
+
time_min (str, optional): The minimum time to fetch events from.
|
|
164
|
+
If not provided, defaults to the current time.
|
|
165
|
+
(default: :obj:`None`)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Union[List[Dict[str, Any]], Dict[str, Any]]: A list of
|
|
169
|
+
dictionaries, each containing details of an event, or a
|
|
170
|
+
dictionary with an error message.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: If the event retrieval fails.
|
|
174
|
+
"""
|
|
175
|
+
if time_min is None:
|
|
176
|
+
time_min = (
|
|
177
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat() + 'Z'
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
if not (time_min.endswith('Z')):
|
|
181
|
+
time_min = time_min + 'Z'
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
events_result = (
|
|
185
|
+
self.service.events()
|
|
186
|
+
.list(
|
|
187
|
+
calendarId='primary',
|
|
188
|
+
timeMin=time_min,
|
|
189
|
+
maxResults=max_results,
|
|
190
|
+
singleEvents=True,
|
|
191
|
+
orderBy='startTime',
|
|
192
|
+
)
|
|
193
|
+
.execute()
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
events = events_result.get('items', [])
|
|
197
|
+
|
|
198
|
+
result = []
|
|
199
|
+
for event in events:
|
|
200
|
+
start = event['start'].get(
|
|
201
|
+
'dateTime', event['start'].get('date')
|
|
202
|
+
)
|
|
203
|
+
result.append(
|
|
204
|
+
{
|
|
205
|
+
'Event ID': event['id'],
|
|
206
|
+
'Summary': event.get('summary', 'No Title'),
|
|
207
|
+
'Start Time': start,
|
|
208
|
+
'Link': event.get('htmlLink'),
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return result
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Failed to retrieve events: {e!s}")
|
|
215
|
+
return {"error": f"Failed to retrieve events: {e!s}"}
|
|
216
|
+
|
|
217
|
+
def update_event(
|
|
218
|
+
self,
|
|
219
|
+
event_id: str,
|
|
220
|
+
event_title: Optional[str] = None,
|
|
221
|
+
start_time: Optional[str] = None,
|
|
222
|
+
end_time: Optional[str] = None,
|
|
223
|
+
description: Optional[str] = None,
|
|
224
|
+
location: Optional[str] = None,
|
|
225
|
+
attendees_email: Optional[List[str]] = None,
|
|
226
|
+
) -> Dict[str, Any]:
|
|
227
|
+
r"""Updates an existing event in the user's primary Google Calendar.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
event_id (str): The ID of the event to update.
|
|
231
|
+
event_title (Optional[str]): New title of the event.
|
|
232
|
+
(default: :obj:`None`)
|
|
233
|
+
start_time (Optional[str]): New start time in ISO format
|
|
234
|
+
(YYYY-MM-DDTHH:MM:SSZ).
|
|
235
|
+
(default: :obj:`None`)
|
|
236
|
+
end_time (Optional[str]): New end time in ISO format
|
|
237
|
+
(YYYY-MM-DDTHH:MM:SSZ).
|
|
238
|
+
(default: :obj:`None`)
|
|
239
|
+
description (Optional[str]): New description of the event.
|
|
240
|
+
(default: :obj:`None`)
|
|
241
|
+
location (Optional[str]): New location of the event.
|
|
242
|
+
(default: :obj:`None`)
|
|
243
|
+
attendees_email (Optional[List[str]]): List of email addresses.
|
|
244
|
+
(default: :obj:`None`)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dict[str, Any]: A dictionary containing details of the updated
|
|
248
|
+
event.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If the event update fails.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
event = (
|
|
255
|
+
self.service.events()
|
|
256
|
+
.get(calendarId='primary', eventId=event_id)
|
|
257
|
+
.execute()
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Update fields that are provided
|
|
261
|
+
if event_title:
|
|
262
|
+
event['summary'] = event_title
|
|
263
|
+
if description:
|
|
264
|
+
event['description'] = description
|
|
265
|
+
if location:
|
|
266
|
+
event['location'] = location
|
|
267
|
+
if start_time:
|
|
268
|
+
event['start']['dateTime'] = start_time
|
|
269
|
+
if end_time:
|
|
270
|
+
event['end']['dateTime'] = end_time
|
|
271
|
+
if attendees_email:
|
|
272
|
+
event['attendees'] = [
|
|
273
|
+
{'email': email} for email in attendees_email
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
updated_event = (
|
|
277
|
+
self.service.events()
|
|
278
|
+
.update(calendarId='primary', eventId=event_id, body=event)
|
|
279
|
+
.execute()
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
'Event ID': updated_event.get('id'),
|
|
284
|
+
'Summary': updated_event.get('summary'),
|
|
285
|
+
'Start Time': updated_event.get('start', {}).get('dateTime'),
|
|
286
|
+
'End Time': updated_event.get('end', {}).get('dateTime'),
|
|
287
|
+
'Link': updated_event.get('htmlLink'),
|
|
288
|
+
'Attendees': [
|
|
289
|
+
attendee.get('email')
|
|
290
|
+
for attendee in updated_event.get('attendees', [])
|
|
291
|
+
],
|
|
292
|
+
}
|
|
293
|
+
except Exception:
|
|
294
|
+
raise ValueError("Failed to update event")
|
|
295
|
+
|
|
296
|
+
def delete_event(self, event_id: str) -> str:
|
|
297
|
+
r"""Deletes an event from the user's primary Google Calendar.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
event_id (str): The ID of the event to delete.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
str: A message indicating the result of the deletion.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: If the event deletion fails.
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
self.service.events().delete(
|
|
310
|
+
calendarId='primary', eventId=event_id
|
|
311
|
+
).execute()
|
|
312
|
+
return f"Event deleted successfully. Event ID: {event_id}"
|
|
313
|
+
except Exception:
|
|
314
|
+
raise ValueError("Failed to delete event")
|
|
315
|
+
|
|
316
|
+
def get_calendar_details(self) -> Dict[str, Any]:
|
|
317
|
+
r"""Retrieves details about the user's primary Google Calendar.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
dict: A dictionary containing details about the calendar.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ValueError: If the calendar details retrieval fails.
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
calendar = (
|
|
327
|
+
self.service.calendars().get(calendarId='primary').execute()
|
|
328
|
+
)
|
|
329
|
+
return {
|
|
330
|
+
'Calendar ID': calendar.get('id'),
|
|
331
|
+
'Summary': calendar.get('summary'),
|
|
332
|
+
'Description': calendar.get('description', 'No description'),
|
|
333
|
+
'Time Zone': calendar.get('timeZone'),
|
|
334
|
+
'Access Role': calendar.get('accessRole'),
|
|
335
|
+
}
|
|
336
|
+
except Exception:
|
|
337
|
+
raise ValueError("Failed to retrieve calendar details")
|
|
338
|
+
|
|
339
|
+
def _get_calendar_service(self):
|
|
340
|
+
r"""Authenticates and creates a Google Calendar service object.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Resource: A Google Calendar API service object.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
ValueError: If authentication fails.
|
|
347
|
+
"""
|
|
348
|
+
from google.auth.transport.requests import Request
|
|
349
|
+
from googleapiclient.discovery import build
|
|
350
|
+
|
|
351
|
+
# Get credentials through authentication
|
|
352
|
+
try:
|
|
353
|
+
creds = self._authenticate()
|
|
354
|
+
|
|
355
|
+
# Refresh token if expired
|
|
356
|
+
if creds and creds.expired and creds.refresh_token:
|
|
357
|
+
creds.refresh(Request())
|
|
358
|
+
|
|
359
|
+
service = build('calendar', 'v3', credentials=creds)
|
|
360
|
+
return service
|
|
361
|
+
except Exception as e:
|
|
362
|
+
raise ValueError(f"Failed to build service: {e!s}")
|
|
363
|
+
|
|
364
|
+
@api_keys_required(
|
|
365
|
+
[
|
|
366
|
+
(None, "GOOGLE_CLIENT_ID"),
|
|
367
|
+
(None, "GOOGLE_CLIENT_SECRET"),
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
def _authenticate(self):
|
|
371
|
+
r"""Gets Google OAuth2 credentials from environment variables.
|
|
372
|
+
|
|
373
|
+
Environment variables needed:
|
|
374
|
+
- GOOGLE_CLIENT_ID: The OAuth client ID
|
|
375
|
+
- GOOGLE_CLIENT_SECRET: The OAuth client secret
|
|
376
|
+
- GOOGLE_REFRESH_TOKEN: (Optional) Refresh token for reauthorization
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Credentials: A Google OAuth2 credentials object.
|
|
380
|
+
"""
|
|
381
|
+
client_id = os.environ.get('GOOGLE_CLIENT_ID')
|
|
382
|
+
client_secret = os.environ.get('GOOGLE_CLIENT_SECRET')
|
|
383
|
+
refresh_token = os.environ.get('GOOGLE_REFRESH_TOKEN')
|
|
384
|
+
token_uri = os.environ.get(
|
|
385
|
+
'GOOGLE_TOKEN_URI', 'https://oauth2.googleapis.com/token'
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
from google.oauth2.credentials import Credentials
|
|
389
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
390
|
+
|
|
391
|
+
# For first-time authentication
|
|
392
|
+
if not refresh_token:
|
|
393
|
+
client_config = {
|
|
394
|
+
"installed": {
|
|
395
|
+
"client_id": client_id,
|
|
396
|
+
"client_secret": client_secret,
|
|
397
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
398
|
+
"token_uri": token_uri,
|
|
399
|
+
"redirect_uris": ["http://localhost"],
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
|
|
404
|
+
creds = flow.run_local_server(port=0)
|
|
405
|
+
|
|
406
|
+
return creds
|
|
407
|
+
else:
|
|
408
|
+
# If we have a refresh token, use it to get credentials
|
|
409
|
+
return Credentials(
|
|
410
|
+
None,
|
|
411
|
+
refresh_token=refresh_token,
|
|
412
|
+
token_uri=token_uri,
|
|
413
|
+
client_id=client_id,
|
|
414
|
+
client_secret=client_secret,
|
|
415
|
+
scopes=SCOPES,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
419
|
+
r"""Returns a list of FunctionTool objects representing the
|
|
420
|
+
functions in the toolkit.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
List[FunctionTool]: A list of FunctionTool objects
|
|
424
|
+
representing the functions in the toolkit.
|
|
425
|
+
"""
|
|
426
|
+
return [
|
|
427
|
+
FunctionTool(self.create_event),
|
|
428
|
+
FunctionTool(self.get_events),
|
|
429
|
+
FunctionTool(self.update_event),
|
|
430
|
+
FunctionTool(self.delete_event),
|
|
431
|
+
FunctionTool(self.get_calendar_details),
|
|
432
|
+
]
|
camel/toolkits/search_toolkit.py
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
import os
|
|
15
15
|
import xml.etree.ElementTree as ET
|
|
16
|
-
from typing import Any, Dict, List, Literal, Optional, TypeAlias, Union
|
|
16
|
+
from typing import Any, Dict, List, Literal, Optional, TypeAlias, Union, cast
|
|
17
17
|
|
|
18
18
|
import requests
|
|
19
19
|
|
|
@@ -947,6 +947,123 @@ class SearchToolkit(BaseToolkit):
|
|
|
947
947
|
except Exception as e:
|
|
948
948
|
return {"error": f"Bing scraping error: {e!s}"}
|
|
949
949
|
|
|
950
|
+
@api_keys_required([(None, 'EXA_API_KEY')])
|
|
951
|
+
def search_exa(
|
|
952
|
+
self,
|
|
953
|
+
query: str,
|
|
954
|
+
search_type: Literal["auto", "neural", "keyword"] = "auto",
|
|
955
|
+
category: Optional[
|
|
956
|
+
Literal[
|
|
957
|
+
"company",
|
|
958
|
+
"research paper",
|
|
959
|
+
"news",
|
|
960
|
+
"pdf",
|
|
961
|
+
"github",
|
|
962
|
+
"tweet",
|
|
963
|
+
"personal site",
|
|
964
|
+
"linkedin profile",
|
|
965
|
+
"financial report",
|
|
966
|
+
]
|
|
967
|
+
] = None,
|
|
968
|
+
num_results: int = 10,
|
|
969
|
+
include_text: Optional[List[str]] = None,
|
|
970
|
+
exclude_text: Optional[List[str]] = None,
|
|
971
|
+
use_autoprompt: bool = True,
|
|
972
|
+
text: bool = False,
|
|
973
|
+
) -> Dict[str, Any]:
|
|
974
|
+
r"""Use Exa search API to perform intelligent web search with optional
|
|
975
|
+
content extraction.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
query (str): The search query string.
|
|
979
|
+
search_type (Literal["auto", "neural", "keyword"]): The type of
|
|
980
|
+
search to perform. "auto" automatically decides between keyword
|
|
981
|
+
and neural search. (default: :obj:`"auto"`)
|
|
982
|
+
category (Optional[Literal]): Category to focus the search on, such
|
|
983
|
+
as "research paper" or "news". (default: :obj:`None`)
|
|
984
|
+
num_results (int): Number of results to return (max 100).
|
|
985
|
+
(default: :obj:`10`)
|
|
986
|
+
include_text (Optional[List[str]]): Strings that must be present in
|
|
987
|
+
webpage text. Limited to 1 string of up to 5 words.
|
|
988
|
+
(default: :obj:`None`)
|
|
989
|
+
exclude_text (Optional[List[str]]): Strings that must not be
|
|
990
|
+
present in webpage text. Limited to 1 string of up to 5 words.
|
|
991
|
+
(default: :obj:`None`)
|
|
992
|
+
use_autoprompt (bool): Whether to use Exa's autoprompt feature to
|
|
993
|
+
enhance the query. (default: :obj:`True`)
|
|
994
|
+
text (bool): Whether to include webpage contents in results.
|
|
995
|
+
(default: :obj:`False`)
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
Dict[str, Any]: A dict containing search results and metadata:
|
|
999
|
+
- requestId (str): Unique identifier for the request
|
|
1000
|
+
- autopromptString (str): Generated autoprompt if enabled
|
|
1001
|
+
- autoDate (str): Timestamp of autoprompt generation
|
|
1002
|
+
- resolvedSearchType (str): The actual search type used
|
|
1003
|
+
- results (List[Dict]): List of search results with metadata
|
|
1004
|
+
- searchType (str): The search type that was selected
|
|
1005
|
+
- costDollars (Dict): Breakdown of API costs
|
|
1006
|
+
"""
|
|
1007
|
+
from exa_py import Exa
|
|
1008
|
+
|
|
1009
|
+
EXA_API_KEY = os.getenv("EXA_API_KEY")
|
|
1010
|
+
|
|
1011
|
+
try:
|
|
1012
|
+
exa = Exa(EXA_API_KEY)
|
|
1013
|
+
|
|
1014
|
+
if num_results is not None and not 0 < num_results <= 100:
|
|
1015
|
+
raise ValueError("num_results must be between 1 and 100")
|
|
1016
|
+
|
|
1017
|
+
if include_text is not None:
|
|
1018
|
+
if len(include_text) > 1:
|
|
1019
|
+
raise ValueError("include_text can only contain 1 string")
|
|
1020
|
+
if len(include_text[0].split()) > 5:
|
|
1021
|
+
raise ValueError(
|
|
1022
|
+
"include_text string cannot be longer than 5 words"
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
if exclude_text is not None:
|
|
1026
|
+
if len(exclude_text) > 1:
|
|
1027
|
+
raise ValueError("exclude_text can only contain 1 string")
|
|
1028
|
+
if len(exclude_text[0].split()) > 5:
|
|
1029
|
+
raise ValueError(
|
|
1030
|
+
"exclude_text string cannot be longer than 5 words"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Call Exa API with direct parameters
|
|
1034
|
+
if text:
|
|
1035
|
+
results = cast(
|
|
1036
|
+
Dict[str, Any],
|
|
1037
|
+
exa.search_and_contents(
|
|
1038
|
+
query=query,
|
|
1039
|
+
type=search_type,
|
|
1040
|
+
category=category,
|
|
1041
|
+
num_results=num_results,
|
|
1042
|
+
include_text=include_text,
|
|
1043
|
+
exclude_text=exclude_text,
|
|
1044
|
+
use_autoprompt=use_autoprompt,
|
|
1045
|
+
text=True,
|
|
1046
|
+
),
|
|
1047
|
+
)
|
|
1048
|
+
else:
|
|
1049
|
+
results = cast(
|
|
1050
|
+
Dict[str, Any],
|
|
1051
|
+
exa.search(
|
|
1052
|
+
query=query,
|
|
1053
|
+
type=search_type,
|
|
1054
|
+
category=category,
|
|
1055
|
+
num_results=num_results,
|
|
1056
|
+
include_text=include_text,
|
|
1057
|
+
exclude_text=exclude_text,
|
|
1058
|
+
use_autoprompt=use_autoprompt,
|
|
1059
|
+
),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
return results
|
|
1063
|
+
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
return {"error": f"Exa search failed: {e!s}"}
|
|
1066
|
+
|
|
950
1067
|
def get_tools(self) -> List[FunctionTool]:
|
|
951
1068
|
r"""Returns a list of FunctionTool objects representing the
|
|
952
1069
|
functions in the toolkit.
|
|
@@ -966,4 +1083,5 @@ class SearchToolkit(BaseToolkit):
|
|
|
966
1083
|
FunctionTool(self.search_bocha),
|
|
967
1084
|
FunctionTool(self.search_baidu),
|
|
968
1085
|
FunctionTool(self.search_bing),
|
|
1086
|
+
FunctionTool(self.search_exa),
|
|
969
1087
|
]
|