camel-ai 0.2.42__py3-none-any.whl → 0.2.43__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/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/tic_tac_toe.py +518 -0
- 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/vllm_model.py +140 -57
- camel/societies/workforce/workforce.py +26 -3
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/browser_toolkit.py +7 -3
- camel/toolkits/google_calendar_toolkit.py +432 -0
- camel/toolkits/search_toolkit.py +119 -1
- camel/types/enums.py +68 -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.43.dist-info}/METADATA +21 -2
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/RECORD +39 -32
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
]
|