camel-ai 0.2.6__py3-none-any.whl → 0.2.7__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 (47) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +107 -22
  3. camel/configs/__init__.py +6 -0
  4. camel/configs/base_config.py +21 -0
  5. camel/configs/gemini_config.py +17 -9
  6. camel/configs/qwen_config.py +91 -0
  7. camel/configs/yi_config.py +58 -0
  8. camel/generators.py +93 -0
  9. camel/interpreters/docker_interpreter.py +5 -0
  10. camel/interpreters/ipython_interpreter.py +2 -1
  11. camel/loaders/__init__.py +2 -0
  12. camel/loaders/apify_reader.py +223 -0
  13. camel/memories/agent_memories.py +24 -1
  14. camel/messages/base.py +38 -0
  15. camel/models/__init__.py +4 -0
  16. camel/models/model_factory.py +6 -0
  17. camel/models/qwen_model.py +139 -0
  18. camel/models/yi_model.py +138 -0
  19. camel/prompts/image_craft.py +8 -0
  20. camel/prompts/video_description_prompt.py +8 -0
  21. camel/retrievers/vector_retriever.py +5 -1
  22. camel/societies/role_playing.py +29 -18
  23. camel/societies/workforce/base.py +7 -1
  24. camel/societies/workforce/task_channel.py +10 -0
  25. camel/societies/workforce/utils.py +6 -0
  26. camel/societies/workforce/worker.py +2 -0
  27. camel/storages/vectordb_storages/qdrant.py +147 -24
  28. camel/tasks/task.py +15 -0
  29. camel/terminators/base.py +4 -0
  30. camel/terminators/response_terminator.py +1 -0
  31. camel/terminators/token_limit_terminator.py +1 -0
  32. camel/toolkits/__init__.py +4 -1
  33. camel/toolkits/base.py +9 -0
  34. camel/toolkits/data_commons_toolkit.py +360 -0
  35. camel/toolkits/function_tool.py +174 -7
  36. camel/toolkits/github_toolkit.py +175 -176
  37. camel/toolkits/google_scholar_toolkit.py +36 -7
  38. camel/toolkits/notion_toolkit.py +279 -0
  39. camel/toolkits/search_toolkit.py +164 -36
  40. camel/types/enums.py +88 -0
  41. camel/types/unified_model_type.py +10 -0
  42. camel/utils/commons.py +2 -1
  43. camel/utils/constants.py +2 -0
  44. {camel_ai-0.2.6.dist-info → camel_ai-0.2.7.dist-info}/METADATA +129 -79
  45. {camel_ai-0.2.6.dist-info → camel_ai-0.2.7.dist-info}/RECORD +47 -40
  46. {camel_ai-0.2.6.dist-info → camel_ai-0.2.7.dist-info}/LICENSE +0 -0
  47. {camel_ai-0.2.6.dist-info → camel_ai-0.2.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,279 @@
1
+ # =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========
14
+ import os
15
+ from typing import List, Optional, cast
16
+
17
+ from camel.toolkits import FunctionTool
18
+ from camel.toolkits.base import BaseToolkit
19
+
20
+
21
+ def get_plain_text_from_rich_text(rich_text: List[dict]) -> str:
22
+ r"""Extracts plain text from a list of rich text elements.
23
+
24
+ Args:
25
+ rich_text: A list of dictionaries representing rich text elements.
26
+ Each dictionary should contain a key named "plain_text" with
27
+ the plain text content.
28
+
29
+ Returns:
30
+ str: A string containing the combined plain text from all elements,
31
+ joined together.
32
+ """
33
+ plain_texts = [element.get("plain_text", "") for element in rich_text]
34
+ return "".join(plain_texts)
35
+
36
+
37
+ def get_media_source_text(block: dict) -> str:
38
+ r"""Extracts the source URL and optional caption from a
39
+ Notion media block.
40
+
41
+ Args:
42
+ block: A dictionary representing a Notion media block.
43
+
44
+ Returns:
45
+ A string containing the source URL and caption (if available),
46
+ separated by a colon.
47
+ """
48
+ block_type = block.get("type", "Unknown Type")
49
+ block_content = block.get(block_type, {})
50
+
51
+ # Extract source URL based on available types
52
+ source = (
53
+ block_content.get("external", {}).get("url")
54
+ or block_content.get("file", {}).get("url")
55
+ or block_content.get(
56
+ "url", "[Missing case for media block types]: " + block_type
57
+ )
58
+ )
59
+
60
+ # Extract caption if available
61
+ caption_elements = block_content.get("caption", [])
62
+ if caption_elements:
63
+ caption = get_plain_text_from_rich_text(caption_elements)
64
+ return f"{caption}: {source}"
65
+
66
+ return source
67
+
68
+
69
+ class NotionToolkit(BaseToolkit):
70
+ r"""A toolkit for retrieving information from the user's notion pages.
71
+
72
+ Attributes:
73
+ notion_token (Optional[str], optional): The notion_token used to
74
+ interact with notion APIs.(default: :obj:`None`)
75
+ notion_client (module): The notion module for interacting with
76
+ the notion APIs.
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ notion_token: Optional[str] = None,
82
+ ) -> None:
83
+ r"""Initializes the NotionToolkit.
84
+
85
+ Args:
86
+ notion_token (Optional[str], optional): The optional notion_token
87
+ used to interact with notion APIs.(default: :obj:`None`)
88
+ """
89
+ from notion_client import Client
90
+
91
+ self.notion_token = notion_token or os.environ.get("NOTION_TOKEN")
92
+ self.notion_client = Client(auth=self.notion_token)
93
+
94
+ def list_all_users(self) -> List[dict]:
95
+ r"""Lists all users via the Notion integration.
96
+
97
+ Returns:
98
+ List[dict]: A list of user objects with type, name, and workspace.
99
+ """
100
+ all_users_info: List[dict] = []
101
+ cursor = None
102
+
103
+ while True:
104
+ response = cast(
105
+ dict,
106
+ self.notion_client.users.list(start_cursor=cursor),
107
+ )
108
+ all_users_info.extend(response["results"])
109
+
110
+ if not response["has_more"]:
111
+ break
112
+
113
+ cursor = response["next_cursor"]
114
+
115
+ formatted_users = [
116
+ {
117
+ "type": user["type"],
118
+ "name": user["name"],
119
+ "workspace": user.get(user.get("type"), {}).get(
120
+ "workspace_name", ""
121
+ ),
122
+ }
123
+ for user in all_users_info
124
+ ]
125
+
126
+ return formatted_users
127
+
128
+ def list_all_pages(self) -> List[dict]:
129
+ r"""Lists all pages in the Notion workspace.
130
+
131
+ Returns:
132
+ List[dict]: A list of page objects with title and id.
133
+ """
134
+ all_pages_info: List[dict] = []
135
+ cursor = None
136
+
137
+ while True:
138
+ response = cast(
139
+ dict,
140
+ self.notion_client.search(
141
+ filter={"property": "object", "value": "page"},
142
+ start_cursor=cursor,
143
+ ),
144
+ )
145
+ all_pages_info.extend(response["results"])
146
+
147
+ if not response["has_more"]:
148
+ break
149
+
150
+ cursor = response["next_cursor"]
151
+
152
+ formatted_pages = [
153
+ {
154
+ "id": page.get("id"),
155
+ "title": next(
156
+ (
157
+ title.get("text", {}).get("content")
158
+ for title in page["properties"]
159
+ .get("title", {})
160
+ .get("title", [])
161
+ if title["type"] == "text"
162
+ ),
163
+ None,
164
+ ),
165
+ }
166
+ for page in all_pages_info
167
+ ]
168
+
169
+ return formatted_pages
170
+
171
+ def get_notion_block_text_content(self, block_id: str) -> str:
172
+ r"""Retrieves the text content of a Notion block.
173
+
174
+ Args:
175
+ block_id (str): The ID of the Notion block to retrieve.
176
+
177
+ Returns:
178
+ str: The text content of a Notion block, containing all
179
+ the sub blocks.
180
+ """
181
+ blocks: List[dict] = []
182
+ cursor = None
183
+
184
+ while True:
185
+ response = cast(
186
+ dict,
187
+ self.notion_client.blocks.children.list(
188
+ block_id=block_id, start_cursor=cursor
189
+ ),
190
+ )
191
+ blocks.extend(response["results"])
192
+
193
+ if not response["has_more"]:
194
+ break
195
+
196
+ cursor = response["next_cursor"]
197
+
198
+ block_text_content = " ".join(
199
+ [self.get_text_from_block(sub_block) for sub_block in blocks]
200
+ )
201
+
202
+ return block_text_content
203
+
204
+ def get_text_from_block(self, block: dict) -> str:
205
+ r"""Extracts plain text from a Notion block based on its type.
206
+
207
+ Args:
208
+ block (dict): A dictionary representing a Notion block.
209
+
210
+ Returns:
211
+ str: A string containing the extracted plain text and block type.
212
+ """
213
+ # Get rich text for supported block types
214
+ if block.get(block.get("type"), {}).get("rich_text"):
215
+ # Empty string if it's an empty line
216
+ text = get_plain_text_from_rich_text(
217
+ block[block["type"]]["rich_text"]
218
+ )
219
+ else:
220
+ # Handle block types by case
221
+ block_type = block.get("type")
222
+ if block_type == "unsupported":
223
+ text = "[Unsupported block type]"
224
+ elif block_type == "bookmark":
225
+ text = block["bookmark"]["url"]
226
+ elif block_type == "child_database":
227
+ text = block["child_database"]["title"]
228
+ # Use other API endpoints for full database data
229
+ elif block_type == "child_page":
230
+ text = block["child_page"]["title"]
231
+ elif block_type in ("embed", "video", "file", "image", "pdf"):
232
+ text = get_media_source_text(block)
233
+ elif block_type == "equation":
234
+ text = block["equation"]["expression"]
235
+ elif block_type == "link_preview":
236
+ text = block["link_preview"]["url"]
237
+ elif block_type == "synced_block":
238
+ if block["synced_block"].get("synced_from"):
239
+ text = (
240
+ f"This block is synced with a block with ID: "
241
+ f"""
242
+ {block['synced_block']['synced_from']
243
+ [block['synced_block']['synced_from']['type']]}
244
+ """
245
+ )
246
+ else:
247
+ text = (
248
+ "Source sync block that another"
249
+ + "blocked is synced with."
250
+ )
251
+ elif block_type == "table":
252
+ text = f"Table width: {block['table']['table_width']}"
253
+ # Fetch children for full table data
254
+ elif block_type == "table_of_contents":
255
+ text = f"ToC color: {block['table_of_contents']['color']}"
256
+ elif block_type in ("breadcrumb", "column_list", "divider"):
257
+ text = "No text available"
258
+ else:
259
+ text = "[Needs case added]"
260
+
261
+ # Query children for blocks with children
262
+ if block.get("has_children"):
263
+ text += self.get_notion_block_text_content(block["id"])
264
+
265
+ return text
266
+
267
+ def get_tools(self) -> List[FunctionTool]:
268
+ r"""Returns a list of FunctionTool objects representing the
269
+ functions in the toolkit.
270
+
271
+ Returns:
272
+ List[FunctionTool]: A list of FunctionTool objects
273
+ representing the functions in the toolkit.
274
+ """
275
+ return [
276
+ FunctionTool(self.list_all_pages),
277
+ FunctionTool(self.list_all_users),
278
+ FunctionTool(self.get_notion_block_text_content),
279
+ ]
@@ -12,10 +12,14 @@
12
12
  # limitations under the License.
13
13
  # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
14
14
  import os
15
- from typing import Any, Dict, List
15
+ import xml.etree.ElementTree as ET
16
+ from typing import Any, Dict, List, Union
17
+
18
+ import requests
16
19
 
17
20
  from camel.toolkits.base import BaseToolkit
18
21
  from camel.toolkits.function_tool import FunctionTool
22
+ from camel.utils import api_keys_required, dependencies_required
19
23
 
20
24
 
21
25
  class SearchToolkit(BaseToolkit):
@@ -25,6 +29,7 @@ class SearchToolkit(BaseToolkit):
25
29
  search engines like Google, DuckDuckGo, Wikipedia and Wolfram Alpha.
26
30
  """
27
31
 
32
+ @dependencies_required("wikipedia")
28
33
  def search_wiki(self, entity: str) -> str:
29
34
  r"""Search the entity in WikiPedia and return the summary of the
30
35
  required page, containing factual information about
@@ -37,13 +42,7 @@ class SearchToolkit(BaseToolkit):
37
42
  str: The search result. If the page corresponding to the entity
38
43
  exists, return the summary of this entity in a string.
39
44
  """
40
- try:
41
- import wikipedia
42
- except ImportError:
43
- raise ImportError(
44
- "Please install `wikipedia` first. You can install it "
45
- "by running `pip install wikipedia`."
46
- )
45
+ import wikipedia
47
46
 
48
47
  result: str
49
48
 
@@ -64,6 +63,7 @@ class SearchToolkit(BaseToolkit):
64
63
 
65
64
  return result
66
65
 
66
+ @dependencies_required("duckduckgo_search")
67
67
  def search_duckduckgo(
68
68
  self, query: str, source: str = "text", max_results: int = 5
69
69
  ) -> List[Dict[str, Any]]:
@@ -151,6 +151,7 @@ class SearchToolkit(BaseToolkit):
151
151
  # If no answer found, return an empty list
152
152
  return responses
153
153
 
154
+ @api_keys_required("GOOGLE_API_KEY", "SEARCH_ENGINE_ID")
154
155
  def search_google(
155
156
  self, query: str, num_result_pages: int = 5
156
157
  ) -> List[Dict[str, Any]]:
@@ -251,7 +252,10 @@ class SearchToolkit(BaseToolkit):
251
252
  # If no answer found, return an empty list
252
253
  return responses
253
254
 
254
- def query_wolfram_alpha(self, query: str, is_detailed: bool) -> str:
255
+ @dependencies_required("wolframalpha")
256
+ def query_wolfram_alpha(
257
+ self, query: str, is_detailed: bool = False
258
+ ) -> Union[str, Dict[str, Any]]:
255
259
  r"""Queries Wolfram|Alpha and returns the result. Wolfram|Alpha is an
256
260
  answer engine developed by Wolfram Research. It is offered as an online
257
261
  service that answers factual queries by computing answers from
@@ -259,19 +263,16 @@ class SearchToolkit(BaseToolkit):
259
263
 
260
264
  Args:
261
265
  query (str): The query to send to Wolfram Alpha.
262
- is_detailed (bool): Whether to include additional details in the
263
- result.
266
+ is_detailed (bool): Whether to include additional details
267
+ including step by step information in the result.
268
+ (default::obj:`False`)
264
269
 
265
270
  Returns:
266
- str: The result from Wolfram Alpha, formatted as a string.
271
+ Union[str, Dict[str, Any]]: The result from Wolfram Alpha.
272
+ Returns a string if `is_detailed` is False, otherwise returns
273
+ a dictionary with detailed information.
267
274
  """
268
- try:
269
- import wolframalpha
270
- except ImportError:
271
- raise ImportError(
272
- "Please install `wolframalpha` first. You can install it by"
273
- " running `pip install wolframalpha`."
274
- )
275
+ import wolframalpha
275
276
 
276
277
  WOLFRAMALPHA_APP_ID = os.environ.get('WOLFRAMALPHA_APP_ID')
277
278
  if not WOLFRAMALPHA_APP_ID:
@@ -284,28 +285,154 @@ class SearchToolkit(BaseToolkit):
284
285
  try:
285
286
  client = wolframalpha.Client(WOLFRAMALPHA_APP_ID)
286
287
  res = client.query(query)
287
- assumption = next(res.pods).text or "No assumption made."
288
- answer = next(res.results).text or "No answer found."
288
+
289
289
  except Exception as e:
290
- if isinstance(e, StopIteration):
291
- return "Wolfram Alpha wasn't able to answer it"
292
- else:
293
- error_message = (
294
- f"Wolfram Alpha wasn't able to answer it" f"{e!s}."
295
- )
296
- return error_message
290
+ return f"Wolfram Alpha wasn't able to answer it. Error: {e}"
297
291
 
298
- result = f"Assumption:\n{assumption}\n\nAnswer:\n{answer}"
292
+ pased_result = self._parse_wolfram_result(res)
299
293
 
300
- # Add additional details in the result
301
294
  if is_detailed:
302
- result += '\n'
303
- for pod in res.pods:
304
- result += '\n' + pod['@title'] + ':\n'
305
- for sub in pod.subpods:
306
- result += (sub.plaintext or "None") + '\n'
295
+ step_info = self._get_wolframalpha_step_by_step_solution(
296
+ WOLFRAMALPHA_APP_ID, query
297
+ )
298
+ pased_result["steps"] = step_info
299
+ return pased_result
300
+
301
+ return pased_result["final_answer"]
302
+
303
+ def _parse_wolfram_result(self, result) -> Dict[str, Any]:
304
+ r"""Parses a Wolfram Alpha API result into a structured dictionary
305
+ format.
306
+
307
+ Args:
308
+ result: The API result returned from a Wolfram Alpha
309
+ query, structured with multiple pods, each containing specific
310
+ information related to the query.
311
+
312
+ Returns:
313
+ dict: A structured dictionary with the original query and the
314
+ final answer.
315
+ """
316
+
317
+ # Extract the original query
318
+ query = result.get('@inputstring', '')
319
+
320
+ # Initialize a dictionary to hold structured output
321
+ output = {"query": query, "pod_info": [], "final_answer": None}
322
+
323
+ # Loop through each pod to extract the details
324
+ for pod in result.get('pod', []):
325
+ pod_info = {
326
+ "title": pod.get('@title', ''),
327
+ "description": pod.get('subpod', {}).get('plaintext', ''),
328
+ "image_url": pod.get('subpod', {})
329
+ .get('img', {})
330
+ .get('@src', ''),
331
+ }
332
+
333
+ # Add to steps list
334
+ output["pod_info"].append(pod_info)
335
+
336
+ # Get final answer
337
+ if pod.get('@primary', False):
338
+ output["final_answer"] = pod_info["description"]
307
339
 
308
- return result.rstrip() # Remove trailing whitespace
340
+ return output
341
+
342
+ def _get_wolframalpha_step_by_step_solution(
343
+ self, app_id: str, query: str
344
+ ) -> dict:
345
+ r"""Retrieve a step-by-step solution from the Wolfram Alpha API for a
346
+ given query.
347
+
348
+ Args:
349
+ app_id (str): Your Wolfram Alpha API application ID.
350
+ query (str): The mathematical or computational query to solve.
351
+
352
+ Returns:
353
+ dict: The step-by-step solution response text from the Wolfram
354
+ Alpha API.
355
+ """
356
+ # Define the base URL
357
+ url = "https://api.wolframalpha.com/v2/query"
358
+
359
+ # Set up the query parameters
360
+ params = {
361
+ 'appid': app_id,
362
+ 'input': query,
363
+ 'podstate': ['Result__Step-by-step solution', 'Show all steps'],
364
+ 'format': 'plaintext',
365
+ }
366
+
367
+ # Send the request
368
+ response = requests.get(url, params=params)
369
+ root = ET.fromstring(response.text)
370
+
371
+ # Extracting step-by-step hints and removing 'Hint: |'
372
+ steps = []
373
+ for subpod in root.findall(
374
+ ".//pod[@title='Results']//subpod[stepbystepcontenttype='SBSHintStep']//plaintext"
375
+ ):
376
+ if subpod.text:
377
+ step_text = subpod.text.strip()
378
+ cleaned_step = step_text.replace('Hint: |', '').strip()
379
+ steps.append(cleaned_step)
380
+
381
+ # Structuring the steps into a dictionary
382
+ structured_steps = {}
383
+ for i, step in enumerate(steps, start=1):
384
+ structured_steps[f"step{i}"] = step
385
+
386
+ return structured_steps
387
+
388
+ def tavily_search(
389
+ self, query: str, num_results: int = 5, **kwargs
390
+ ) -> List[Dict[str, Any]]:
391
+ r"""Use Tavily Search API to search information for the given query.
392
+
393
+ Args:
394
+ query (str): The query to be searched.
395
+ num_results (int): The number of search results to retrieve
396
+ (default is `5`).
397
+ **kwargs: Additional optional parameters supported by Tavily's API:
398
+ - search_depth (str): "basic" or "advanced" search depth.
399
+ - topic (str): The search category, e.g., "general" or "news."
400
+ - days (int): Time frame in days for news-related searches.
401
+ - max_results (int): Max number of results to return
402
+ (overrides `num_results`).
403
+ See https://docs.tavily.com/docs/python-sdk/tavily-search/
404
+ api-reference for details.
405
+
406
+ Returns:
407
+ List[Dict[str, Any]]: A list of dictionaries representing search
408
+ results. Each dictionary contains:
409
+ - 'result_id' (int): The result's index.
410
+ - 'title' (str): The title of the result.
411
+ - 'description' (str): A brief description of the result.
412
+ - 'long_description' (str): Detailed information, if available.
413
+ - 'url' (str): The URL of the result.
414
+ - 'content' (str): Relevant content from the search result.
415
+ - 'images' (list): A list of related images (if
416
+ `include_images` is True).
417
+ - 'published_date' (str): Publication date for news topics
418
+ (if available).
419
+ """
420
+ from tavily import TavilyClient # type: ignore[import-untyped]
421
+
422
+ Tavily_API_KEY = os.getenv("TAVILY_API_KEY")
423
+ if not Tavily_API_KEY:
424
+ raise ValueError(
425
+ "`TAVILY_API_KEY` not found in environment variables. "
426
+ "Get `TAVILY_API_KEY` here: `https://www.tavily.com/api/`."
427
+ )
428
+
429
+ client = TavilyClient(Tavily_API_KEY)
430
+
431
+ try:
432
+ results = client.search(query, max_results=num_results, **kwargs)
433
+ return results
434
+ except Exception as e:
435
+ return [{"error": f"An unexpected error occurred: {e!s}"}]
309
436
 
310
437
  def get_tools(self) -> List[FunctionTool]:
311
438
  r"""Returns a list of FunctionTool objects representing the
@@ -320,6 +447,7 @@ class SearchToolkit(BaseToolkit):
320
447
  FunctionTool(self.search_google),
321
448
  FunctionTool(self.search_duckduckgo),
322
449
  FunctionTool(self.query_wolfram_alpha),
450
+ FunctionTool(self.tavily_search),
323
451
  ]
324
452
 
325
453