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.

Files changed (39) hide show
  1. camel/__init__.py +1 -1
  2. camel/configs/__init__.py +3 -0
  3. camel/configs/anthropic_config.py +2 -24
  4. camel/configs/ppio_config.py +102 -0
  5. camel/configs/reka_config.py +1 -7
  6. camel/configs/samba_config.py +1 -7
  7. camel/configs/togetherai_config.py +1 -7
  8. camel/embeddings/__init__.py +4 -0
  9. camel/embeddings/azure_embedding.py +119 -0
  10. camel/embeddings/together_embedding.py +136 -0
  11. camel/environments/__init__.py +3 -0
  12. camel/environments/multi_step.py +12 -10
  13. camel/environments/tic_tac_toe.py +518 -0
  14. camel/loaders/__init__.py +2 -0
  15. camel/loaders/crawl4ai_reader.py +230 -0
  16. camel/models/__init__.py +2 -0
  17. camel/models/azure_openai_model.py +10 -2
  18. camel/models/base_model.py +111 -28
  19. camel/models/cohere_model.py +5 -1
  20. camel/models/deepseek_model.py +4 -0
  21. camel/models/gemini_model.py +8 -2
  22. camel/models/model_factory.py +3 -0
  23. camel/models/ollama_model.py +8 -2
  24. camel/models/openai_compatible_model.py +8 -2
  25. camel/models/openai_model.py +16 -4
  26. camel/models/ppio_model.py +184 -0
  27. camel/models/vllm_model.py +140 -57
  28. camel/societies/workforce/workforce.py +26 -3
  29. camel/toolkits/__init__.py +2 -0
  30. camel/toolkits/browser_toolkit.py +7 -3
  31. camel/toolkits/google_calendar_toolkit.py +432 -0
  32. camel/toolkits/search_toolkit.py +119 -1
  33. camel/types/enums.py +68 -3
  34. camel/types/unified_model_type.py +5 -0
  35. camel/verifiers/python_verifier.py +93 -9
  36. {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/METADATA +21 -2
  37. {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/RECORD +39 -32
  38. {camel_ai-0.2.42.dist-info → camel_ai-0.2.43.dist-info}/WHEEL +0 -0
  39. {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
+ ]
@@ -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
  ]