camel-ai 0.2.21__py3-none-any.whl → 0.2.23__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 (116) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/_types.py +41 -0
  3. camel/agents/_utils.py +188 -0
  4. camel/agents/chat_agent.py +570 -965
  5. camel/agents/knowledge_graph_agent.py +7 -1
  6. camel/agents/multi_hop_generator_agent.py +1 -1
  7. camel/configs/base_config.py +10 -13
  8. camel/configs/deepseek_config.py +4 -30
  9. camel/configs/gemini_config.py +5 -31
  10. camel/configs/openai_config.py +14 -32
  11. camel/configs/qwen_config.py +36 -36
  12. camel/datagen/self_improving_cot.py +81 -3
  13. camel/datagen/self_instruct/filter/instruction_filter.py +19 -3
  14. camel/datagen/self_instruct/self_instruct.py +53 -4
  15. camel/datasets/__init__.py +28 -0
  16. camel/datasets/base.py +969 -0
  17. camel/embeddings/openai_embedding.py +10 -1
  18. camel/environments/__init__.py +16 -0
  19. camel/environments/base.py +503 -0
  20. camel/extractors/__init__.py +16 -0
  21. camel/extractors/base.py +263 -0
  22. camel/interpreters/docker/Dockerfile +12 -0
  23. camel/interpreters/docker_interpreter.py +19 -1
  24. camel/interpreters/subprocess_interpreter.py +42 -17
  25. camel/loaders/__init__.py +2 -0
  26. camel/loaders/mineru_extractor.py +250 -0
  27. camel/memories/agent_memories.py +16 -1
  28. camel/memories/blocks/chat_history_block.py +10 -2
  29. camel/memories/blocks/vectordb_block.py +1 -0
  30. camel/memories/context_creators/score_based.py +20 -3
  31. camel/memories/records.py +10 -0
  32. camel/messages/base.py +8 -8
  33. camel/models/__init__.py +2 -0
  34. camel/models/_utils.py +57 -0
  35. camel/models/aiml_model.py +48 -17
  36. camel/models/anthropic_model.py +41 -3
  37. camel/models/azure_openai_model.py +39 -3
  38. camel/models/base_audio_model.py +92 -0
  39. camel/models/base_model.py +132 -4
  40. camel/models/cohere_model.py +88 -11
  41. camel/models/deepseek_model.py +107 -63
  42. camel/models/fish_audio_model.py +18 -8
  43. camel/models/gemini_model.py +133 -15
  44. camel/models/groq_model.py +72 -10
  45. camel/models/internlm_model.py +14 -3
  46. camel/models/litellm_model.py +9 -2
  47. camel/models/mistral_model.py +42 -5
  48. camel/models/model_manager.py +57 -3
  49. camel/models/moonshot_model.py +33 -4
  50. camel/models/nemotron_model.py +32 -3
  51. camel/models/nvidia_model.py +43 -3
  52. camel/models/ollama_model.py +139 -17
  53. camel/models/openai_audio_models.py +87 -2
  54. camel/models/openai_compatible_model.py +37 -3
  55. camel/models/openai_model.py +158 -46
  56. camel/models/qwen_model.py +61 -4
  57. camel/models/reka_model.py +53 -3
  58. camel/models/samba_model.py +209 -4
  59. camel/models/sglang_model.py +153 -14
  60. camel/models/siliconflow_model.py +16 -3
  61. camel/models/stub_model.py +46 -4
  62. camel/models/togetherai_model.py +38 -3
  63. camel/models/vllm_model.py +37 -3
  64. camel/models/yi_model.py +36 -3
  65. camel/models/zhipuai_model.py +38 -3
  66. camel/retrievers/__init__.py +3 -0
  67. camel/retrievers/hybrid_retrival.py +237 -0
  68. camel/toolkits/__init__.py +20 -3
  69. camel/toolkits/arxiv_toolkit.py +2 -1
  70. camel/toolkits/ask_news_toolkit.py +4 -2
  71. camel/toolkits/audio_analysis_toolkit.py +238 -0
  72. camel/toolkits/base.py +22 -3
  73. camel/toolkits/code_execution.py +2 -0
  74. camel/toolkits/dappier_toolkit.py +2 -1
  75. camel/toolkits/data_commons_toolkit.py +38 -12
  76. camel/toolkits/excel_toolkit.py +172 -0
  77. camel/toolkits/function_tool.py +13 -0
  78. camel/toolkits/github_toolkit.py +5 -1
  79. camel/toolkits/google_maps_toolkit.py +2 -1
  80. camel/toolkits/google_scholar_toolkit.py +2 -0
  81. camel/toolkits/human_toolkit.py +0 -3
  82. camel/toolkits/image_analysis_toolkit.py +202 -0
  83. camel/toolkits/linkedin_toolkit.py +3 -2
  84. camel/toolkits/meshy_toolkit.py +3 -2
  85. camel/toolkits/mineru_toolkit.py +178 -0
  86. camel/toolkits/networkx_toolkit.py +240 -0
  87. camel/toolkits/notion_toolkit.py +2 -0
  88. camel/toolkits/openbb_toolkit.py +3 -2
  89. camel/toolkits/page_script.js +376 -0
  90. camel/toolkits/reddit_toolkit.py +11 -3
  91. camel/toolkits/retrieval_toolkit.py +6 -1
  92. camel/toolkits/semantic_scholar_toolkit.py +2 -1
  93. camel/toolkits/stripe_toolkit.py +8 -2
  94. camel/toolkits/sympy_toolkit.py +44 -1
  95. camel/toolkits/video_analysis_toolkit.py +407 -0
  96. camel/toolkits/{video_toolkit.py → video_download_toolkit.py} +21 -25
  97. camel/toolkits/web_toolkit.py +1307 -0
  98. camel/toolkits/whatsapp_toolkit.py +3 -2
  99. camel/toolkits/zapier_toolkit.py +191 -0
  100. camel/types/__init__.py +2 -2
  101. camel/types/agents/__init__.py +16 -0
  102. camel/types/agents/tool_calling_record.py +52 -0
  103. camel/types/enums.py +3 -0
  104. camel/types/openai_types.py +16 -14
  105. camel/utils/__init__.py +2 -1
  106. camel/utils/async_func.py +2 -2
  107. camel/utils/commons.py +114 -1
  108. camel/verifiers/__init__.py +23 -0
  109. camel/verifiers/base.py +340 -0
  110. camel/verifiers/models.py +82 -0
  111. camel/verifiers/python_verifier.py +202 -0
  112. camel_ai-0.2.23.dist-info/METADATA +671 -0
  113. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23.dist-info}/RECORD +127 -99
  114. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23.dist-info}/WHEEL +1 -1
  115. camel_ai-0.2.21.dist-info/METADATA +0 -528
  116. {camel_ai-0.2.21.dist-info → camel_ai-0.2.23.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,237 @@
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
+ from typing import Any, Collection, Dict, List, Optional, Sequence, Union
15
+
16
+ import numpy as np
17
+
18
+ from camel.embeddings import BaseEmbedding
19
+ from camel.retrievers import BaseRetriever, BM25Retriever, VectorRetriever
20
+ from camel.storages import BaseVectorStorage
21
+
22
+
23
+ class HybridRetriever(BaseRetriever):
24
+ def __init__(
25
+ self,
26
+ embedding_model: Optional[BaseEmbedding] = None,
27
+ vector_storage: Optional[BaseVectorStorage] = None,
28
+ ) -> None:
29
+ r"""Initializes the HybridRetriever with optional embedding model and
30
+ vector storage.
31
+
32
+ Args:
33
+ embedding_model (Optional[BaseEmbedding]): An optional embedding
34
+ model used by the VectorRetriever. Defaults to None.
35
+ vector_storage (Optional[BaseVectorStorage]): An optional vector
36
+ storage used by the VectorRetriever. Defaults to None.
37
+ """
38
+ self.vr = VectorRetriever(embedding_model, vector_storage)
39
+ self.bm25 = BM25Retriever()
40
+
41
+ def process(self, content_input_path: str) -> None:
42
+ r"""Processes the content input path for both vector and BM25
43
+ retrievers.
44
+
45
+ Args:
46
+ content_input_path (str): File path or URL of the content to be
47
+ processed.
48
+
49
+ Raises:
50
+ ValueError: If the content_input_path is empty.
51
+ """
52
+ if not content_input_path:
53
+ raise ValueError("content_input_path cannot be empty.")
54
+
55
+ self.content_input_path = content_input_path
56
+ self.vr.process(content=self.content_input_path)
57
+ self.bm25.process(content_input_path=self.content_input_path)
58
+
59
+ def _sort_rrf_scores(
60
+ self,
61
+ vector_retriever_results: List[Dict[str, Any]],
62
+ bm25_retriever_results: List[Dict[str, Any]],
63
+ top_k: int,
64
+ vector_weight: float,
65
+ bm25_weight: float,
66
+ rank_smoothing_factor: float,
67
+ ) -> List[Dict[str, Union[str, float]]]:
68
+ r"""Sorts and combines results from vector and BM25 retrievers using
69
+ Reciprocal Rank Fusion (RRF).
70
+
71
+ Args:
72
+ vector_retriever_results: A list of dictionaries containing the
73
+ results from the vector retriever, where each dictionary
74
+ contains a 'text' entry.
75
+ bm25_retriever_results: A list of dictionaries containing the
76
+ results from the BM25 retriever, where each dictionary
77
+ contains a 'text' entry.
78
+ top_k: The number of top results to return after sorting by RRF
79
+ score.
80
+ vector_weight: The weight to assign to the vector retriever
81
+ results in the RRF calculation.
82
+ bm25_weight: The weight to assign to the BM25 retriever results in
83
+ the RRF calculation.
84
+ rank_smoothing_factor: A hyperparameter for the RRF calculation
85
+ that helps smooth the rank positions.
86
+
87
+ Returns:
88
+ List[Dict[str, Union[str, float]]]: A list of dictionaries
89
+ representing the sorted results. Each dictionary contains the
90
+ 'text'from the retrieved items and their corresponding 'rrf_score'.
91
+
92
+ Raises:
93
+ ValueError: If any of the input weights are negative.
94
+
95
+ References:
96
+ https://medium.com/@devalshah1619/mathematical-intuition-behind-reciprocal-rank-fusion-rrf-explained-in-2-mins-002df0cc5e2a
97
+ https://colab.research.google.com/drive/1iwVJrN96fiyycxN1pBqWlEr_4EPiGdGy#scrollTo=0qh83qGV2dY8
98
+ """
99
+ text_to_id = {}
100
+ id_to_info = {}
101
+ current_id = 1
102
+
103
+ # Iterate over vector_retriever_results
104
+ for rank, result in enumerate(vector_retriever_results, start=1):
105
+ text = result.get('text', None) # type: ignore[attr-defined]
106
+ if text is None:
107
+ raise KeyError("Each result must contain a 'text' key")
108
+
109
+ if text not in text_to_id:
110
+ text_to_id[text] = current_id
111
+ id_to_info[current_id] = {'text': text, 'vector_rank': rank}
112
+ current_id += 1
113
+ else:
114
+ id_to_info[text_to_id[text]]['vector_rank'] = rank
115
+
116
+ # Iterate over bm25_retriever_results
117
+ for rank, result in enumerate(bm25_retriever_results, start=1):
118
+ text = result['text']
119
+ if text not in text_to_id:
120
+ text_to_id[text] = current_id
121
+ id_to_info[current_id] = {'text': text, 'bm25_rank': rank}
122
+ current_id += 1
123
+ else:
124
+ id_to_info[text_to_id[text]].setdefault('bm25_rank', rank)
125
+
126
+ vector_ranks = np.array(
127
+ [
128
+ info.get('vector_rank', float('inf'))
129
+ for info in id_to_info.values()
130
+ ]
131
+ )
132
+ bm25_ranks = np.array(
133
+ [
134
+ info.get('bm25_rank', float('inf'))
135
+ for info in id_to_info.values()
136
+ ]
137
+ )
138
+
139
+ # Calculate RRF scores
140
+ vector_rrf_scores = vector_weight / (
141
+ rank_smoothing_factor + vector_ranks
142
+ )
143
+ bm25_rrf_scores = bm25_weight / (rank_smoothing_factor + bm25_ranks)
144
+ rrf_scores = vector_rrf_scores + bm25_rrf_scores
145
+
146
+ for idx, (_, info) in enumerate(id_to_info.items()):
147
+ info['rrf_score'] = rrf_scores[idx]
148
+ sorted_results = sorted(
149
+ id_to_info.values(), key=lambda x: x['rrf_score'], reverse=True
150
+ )
151
+ return sorted_results[:top_k]
152
+
153
+ def query(
154
+ self,
155
+ query: str,
156
+ top_k: int = 20,
157
+ vector_weight: float = 0.8,
158
+ bm25_weight: float = 0.2,
159
+ rank_smoothing_factor: int = 60,
160
+ vector_retriever_top_k: int = 50,
161
+ vector_retriever_similarity_threshold: float = 0.5,
162
+ bm25_retriever_top_k: int = 50,
163
+ return_detailed_info: bool = False,
164
+ ) -> Union[
165
+ dict[str, Sequence[Collection[str]]],
166
+ dict[str, Sequence[Union[str, float]]],
167
+ ]:
168
+ r"""Executes a hybrid retrieval query using both vector and BM25
169
+ retrievers.
170
+
171
+ Args:
172
+ query (str): The search query.
173
+ top_k (int): Number of top results to return (default 20).
174
+ vector_weight (float): Weight for vector retriever results in RRF.
175
+ bm25_weight (float): Weight for BM25 retriever results in RRF.
176
+ rank_smoothing_factor (int): RRF hyperparameter for rank smoothing.
177
+ vector_retriever_top_k (int): Top results from vector retriever.
178
+ vector_retriever_similarity_threshold (float): Similarity
179
+ threshold for vector retriever.
180
+ bm25_retriever_top_k (int): Top results from BM25 retriever.
181
+ return_detailed_info (bool): Return detailed info if True.
182
+
183
+ Returns:
184
+ Union[
185
+ dict[str, Sequence[Collection[str]]],
186
+ dict[str, Sequence[Union[str, float]]]
187
+ ]: By default, returns only the text information. If
188
+ `return_detailed_info` is `True`, return detailed information
189
+ including rrf scores.
190
+ """
191
+ if top_k > max(vector_retriever_top_k, bm25_retriever_top_k):
192
+ raise ValueError(
193
+ "top_k needs to be less than or equal to the "
194
+ "maximum value among vector_retriever_top_k and "
195
+ "bm25_retriever_top_k."
196
+ )
197
+ if vector_weight < 0 or bm25_weight < 0:
198
+ raise ValueError(
199
+ "Neither `vector_weight` nor `bm25_weight` can be negative."
200
+ )
201
+
202
+ vr_raw_results: List[Dict[str, Any]] = self.vr.query(
203
+ query=query,
204
+ top_k=vector_retriever_top_k,
205
+ similarity_threshold=vector_retriever_similarity_threshold,
206
+ )
207
+ # if the number of results is less than top_k, return all results
208
+ with_score = [
209
+ info for info in vr_raw_results if 'similarity score' in info
210
+ ]
211
+ vector_retriever_results = sorted(
212
+ with_score, key=lambda x: x['similarity score'], reverse=True
213
+ )
214
+
215
+ bm25_retriever_results = self.bm25.query(
216
+ query=query,
217
+ top_k=bm25_retriever_top_k,
218
+ )
219
+
220
+ all_retrieved_info = self._sort_rrf_scores(
221
+ vector_retriever_results,
222
+ bm25_retriever_results,
223
+ top_k,
224
+ vector_weight,
225
+ bm25_weight,
226
+ rank_smoothing_factor,
227
+ )
228
+
229
+ retrieved_info = {
230
+ "Original Query": query,
231
+ "Retrieved Context": (
232
+ all_retrieved_info
233
+ if return_detailed_info
234
+ else [item['text'] for item in all_retrieved_info] # type: ignore[misc]
235
+ ),
236
+ }
237
+ return retrieved_info
@@ -43,10 +43,19 @@ from .retrieval_toolkit import RetrievalToolkit
43
43
  from .notion_toolkit import NotionToolkit
44
44
  from .human_toolkit import HumanToolkit
45
45
  from .stripe_toolkit import StripeToolkit
46
- from .video_toolkit import VideoDownloaderToolkit
46
+ from .video_download_toolkit import VideoDownloaderToolkit
47
47
  from .dappier_toolkit import DappierToolkit
48
- from .sympy_toolkit import SymPyToolkit
48
+ from .networkx_toolkit import NetworkXToolkit
49
49
  from .semantic_scholar_toolkit import SemanticScholarToolkit
50
+ from .zapier_toolkit import ZapierToolkit
51
+ from .sympy_toolkit import SymPyToolkit
52
+ from .mineru_toolkit import MinerUToolkit
53
+ from .audio_analysis_toolkit import AudioAnalysisToolkit
54
+ from .excel_toolkit import ExcelToolkit
55
+ from .video_analysis_toolkit import VideoAnalysisToolkit
56
+ from .image_analysis_toolkit import ImageAnalysisToolkit
57
+ from .web_toolkit import WebToolkit
58
+
50
59
 
51
60
  __all__ = [
52
61
  'BaseToolkit',
@@ -79,6 +88,14 @@ __all__ = [
79
88
  'MeshyToolkit',
80
89
  'OpenBBToolkit',
81
90
  'DappierToolkit',
82
- 'SymPyToolkit',
91
+ 'NetworkXToolkit',
83
92
  'SemanticScholarToolkit',
93
+ 'ZapierToolkit',
94
+ 'SymPyToolkit',
95
+ 'MinerUToolkit',
96
+ 'AudioAnalysisToolkit',
97
+ 'ExcelToolkit',
98
+ 'VideoAnalysisToolkit',
99
+ 'ImageAnalysisToolkit',
100
+ 'WebToolkit',
84
101
  ]
@@ -28,8 +28,9 @@ class ArxivToolkit(BaseToolkit):
28
28
  """
29
29
 
30
30
  @dependencies_required('arxiv')
31
- def __init__(self) -> None:
31
+ def __init__(self, timeout: Optional[float] = None) -> None:
32
32
  r"""Initializes the ArxivToolkit and sets up the arXiv client."""
33
+ super().__init__(timeout=timeout)
33
34
  import arxiv
34
35
 
35
36
  self.client = arxiv.Client()
@@ -62,11 +62,13 @@ class AskNewsToolkit(BaseToolkit):
62
62
  based on user queries using the AskNews API.
63
63
  """
64
64
 
65
- def __init__(self):
65
+ def __init__(self, timeout: Optional[float] = None):
66
66
  r"""Initialize the AskNewsToolkit with API clients.The API keys and
67
67
  credentials are retrieved from environment variables.
68
68
  """
69
- from asknews_sdk import AskNewsSDK
69
+ super().__init__(timeout=timeout)
70
+
71
+ from asknews_sdk import AskNewsSDK # type: ignore[import-not-found]
70
72
 
71
73
  client_id = os.environ.get("ASKNEWS_CLIENT_ID")
72
74
  client_secret = os.environ.get("ASKNEWS_CLIENT_SECRET")
@@ -0,0 +1,238 @@
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
+ import os
15
+ import uuid
16
+ from typing import List, Optional
17
+ from urllib.parse import urlparse
18
+
19
+ import requests
20
+
21
+ from camel.logger import get_logger
22
+ from camel.messages import BaseMessage
23
+ from camel.models import BaseAudioModel, BaseModelBackend, OpenAIAudioModels
24
+ from camel.toolkits.base import BaseToolkit
25
+ from camel.toolkits.function_tool import FunctionTool
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ def download_file(url: str, cache_dir: str) -> str:
31
+ r"""Download a file from a URL to a local cache directory.
32
+
33
+ Args:
34
+ url (str): The URL of the file to download.
35
+ cache_dir (str): The directory to save the downloaded file.
36
+
37
+ Returns:
38
+ str: The path to the downloaded file.
39
+
40
+ Raises:
41
+ Exception: If the download fails.
42
+ """
43
+ # Create cache directory if it doesn't exist
44
+ os.makedirs(cache_dir, exist_ok=True)
45
+
46
+ # Extract filename from URL or generate a unique one
47
+ parsed_url = urlparse(url)
48
+ filename = os.path.basename(parsed_url.path)
49
+ if not filename:
50
+ # Generate a unique filename if none is provided in the URL
51
+ file_ext = ".mp3" # Default extension
52
+ content_type = None
53
+
54
+ # Try to get the file extension from the content type
55
+ try:
56
+ response = requests.head(url)
57
+ content_type = response.headers.get('Content-Type', '')
58
+ if 'audio/wav' in content_type:
59
+ file_ext = '.wav'
60
+ elif 'audio/mpeg' in content_type:
61
+ file_ext = '.mp3'
62
+ elif 'audio/ogg' in content_type:
63
+ file_ext = '.ogg'
64
+ except Exception:
65
+ pass
66
+
67
+ filename = f"{uuid.uuid4()}{file_ext}"
68
+
69
+ local_path = os.path.join(cache_dir, filename)
70
+
71
+ # Download the file
72
+ response = requests.get(url, stream=True)
73
+ response.raise_for_status()
74
+
75
+ with open(local_path, 'wb') as f:
76
+ for chunk in response.iter_content(chunk_size=8192):
77
+ f.write(chunk)
78
+
79
+ logger.debug(f"Downloaded file from {url} to {local_path}")
80
+ return local_path
81
+
82
+
83
+ class AudioAnalysisToolkit(BaseToolkit):
84
+ r"""A toolkit for audio processing and analysis.
85
+
86
+ This class provides methods for processing, transcribing, and extracting
87
+ information from audio data, including direct question answering about
88
+ audio content.
89
+
90
+ Args:
91
+ cache_dir (Optional[str]): Directory path for caching downloaded audio
92
+ files. If not provided, 'tmp/' will be used. (default: :obj:`None`)
93
+ transcribe_model (Optional[BaseAudioModel]): Model used for audio
94
+ transcription. If not provided, OpenAIAudioModels will be used.
95
+ (default: :obj:`None`)
96
+ audio_reasoning_model (Optional[BaseModelBackend]): Model used for
97
+ audio reasoning and question answering. If not provided, uses the
98
+ default model from ChatAgent. (default: :obj:`None`)
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ cache_dir: Optional[str] = None,
104
+ transcribe_model: Optional[BaseAudioModel] = None,
105
+ audio_reasoning_model: Optional[BaseModelBackend] = None,
106
+ ):
107
+ self.cache_dir = 'tmp/'
108
+ if cache_dir:
109
+ self.cache_dir = cache_dir
110
+
111
+ if transcribe_model:
112
+ self.transcribe_model = transcribe_model
113
+ else:
114
+ self.transcribe_model = OpenAIAudioModels()
115
+ logger.warning(
116
+ "No audio transcription model provided. "
117
+ "Using OpenAIAudioModels."
118
+ )
119
+
120
+ from camel.agents import ChatAgent
121
+
122
+ if audio_reasoning_model:
123
+ self.audio_agent = ChatAgent(model=audio_reasoning_model)
124
+ else:
125
+ self.audio_agent = ChatAgent()
126
+ logger.warning(
127
+ "No audio reasoning model provided. Using default model in"
128
+ " ChatAgent."
129
+ )
130
+
131
+ def audio2text(self, audio_path: str) -> str:
132
+ r"""Transcribe audio to text.
133
+
134
+ Args:
135
+ audio_path (str): The path to the audio file or URL.
136
+
137
+ Returns:
138
+ str: The transcribed text.
139
+ """
140
+ logger.debug(
141
+ f"Calling transcribe_audio method for audio file `{audio_path}`."
142
+ )
143
+
144
+ try:
145
+ audio_transcript = self.transcribe_model.speech_to_text(audio_path)
146
+ if not audio_transcript:
147
+ logger.warning("Audio transcription returned empty result")
148
+ return "No audio transcription available."
149
+ return audio_transcript
150
+ except Exception as e:
151
+ logger.error(f"Audio transcription failed: {e}")
152
+ return "Audio transcription failed."
153
+
154
+ def ask_question_about_audio(self, audio_path: str, question: str) -> str:
155
+ r"""Ask any question about the audio and get the answer using
156
+ multimodal model.
157
+
158
+ Args:
159
+ audio_path (str): The path to the audio file.
160
+ question (str): The question to ask about the audio.
161
+
162
+ Returns:
163
+ str: The answer to the question.
164
+ """
165
+
166
+ logger.debug(
167
+ f"Calling ask_question_about_audio method for audio file \
168
+ `{audio_path}` and question `{question}`."
169
+ )
170
+
171
+ parsed_url = urlparse(audio_path)
172
+ is_url = all([parsed_url.scheme, parsed_url.netloc])
173
+ local_audio_path = audio_path
174
+
175
+ # If the audio is a URL, download it first
176
+ if is_url:
177
+ try:
178
+ local_audio_path = download_file(audio_path, self.cache_dir)
179
+ except Exception as e:
180
+ logger.error(f"Failed to download audio file: {e}")
181
+ return f"Failed to download audio file: {e!s}"
182
+
183
+ # Try direct audio question answering first
184
+ try:
185
+ # Check if the transcribe_model supports audio_question_answering
186
+ if hasattr(self.transcribe_model, 'audio_question_answering'):
187
+ logger.debug("Using direct audio question answering")
188
+ response = self.transcribe_model.audio_question_answering(
189
+ local_audio_path, question
190
+ )
191
+ return response
192
+ except Exception as e:
193
+ logger.warning(
194
+ f"Direct audio question answering failed: {e}. "
195
+ "Falling back to transcription-based approach."
196
+ )
197
+
198
+ # Fallback to transcription-based approach
199
+ try:
200
+ transcript = self.audio2text(local_audio_path)
201
+ reasoning_prompt = f"""
202
+ <speech_transcription_result>{transcript}</
203
+ speech_transcription_result>
204
+
205
+ Please answer the following question based on the speech
206
+ transcription result above:
207
+ <question>{question}</question>
208
+ """
209
+ msg = BaseMessage.make_user_message(
210
+ role_name="User", content=reasoning_prompt
211
+ )
212
+ response = self.audio_agent.step(msg)
213
+
214
+ if not response or not response.msgs:
215
+ logger.error("Model returned empty response")
216
+ return (
217
+ "Failed to generate an answer. "
218
+ "The model returned an empty response."
219
+ )
220
+
221
+ answer = response.msgs[0].content
222
+ return answer
223
+ except Exception as e:
224
+ logger.error(f"Audio question answering failed: {e}")
225
+ return f"Failed to answer question about audio: {e!s}"
226
+
227
+ def get_tools(self) -> List[FunctionTool]:
228
+ r"""Returns a list of FunctionTool objects representing the functions
229
+ in the toolkit.
230
+
231
+ Returns:
232
+ List[FunctionTool]: A list of FunctionTool objects representing the
233
+ functions in the toolkit.
234
+ """
235
+ return [
236
+ FunctionTool(self.ask_question_about_audio),
237
+ FunctionTool(self.audio2text),
238
+ ]
camel/toolkits/base.py CHANGED
@@ -12,14 +12,33 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
- from typing import List
15
+ from typing import List, Optional
16
16
 
17
17
  from camel.toolkits import FunctionTool
18
- from camel.utils import AgentOpsMeta
18
+ from camel.utils import AgentOpsMeta, with_timeout
19
19
 
20
20
 
21
21
  class BaseToolkit(metaclass=AgentOpsMeta):
22
- r"""Base class for toolkits."""
22
+ r"""Base class for toolkits.
23
+
24
+ Args:
25
+ timeout (Optional[float]): The timeout for the toolkit.
26
+ """
27
+
28
+ timeout: Optional[float] = None
29
+
30
+ def __init__(self, timeout: Optional[float] = None):
31
+ # check if timeout is a positive number
32
+ if timeout is not None and timeout <= 0:
33
+ raise ValueError("Timeout must be a positive number.")
34
+ self.timeout = timeout
35
+
36
+ # Add timeout to all callable methods in the toolkit
37
+ def __init_subclass__(cls, **kwargs):
38
+ super().__init_subclass__(**kwargs)
39
+ for attr_name, attr_value in cls.__dict__.items():
40
+ if callable(attr_value) and not attr_name.startswith("__"):
41
+ setattr(cls, attr_name, with_timeout(attr_value))
23
42
 
24
43
  def get_tools(self) -> List[FunctionTool]:
25
44
  r"""Returns a list of FunctionTool objects representing the
@@ -48,7 +48,9 @@ class CodeExecutionToolkit(BaseToolkit):
48
48
  unsafe_mode: bool = False,
49
49
  import_white_list: Optional[List[str]] = None,
50
50
  require_confirm: bool = False,
51
+ timeout: Optional[float] = None,
51
52
  ) -> None:
53
+ super().__init__(timeout=timeout)
52
54
  self.verbose = verbose
53
55
  self.unsafe_mode = unsafe_mode
54
56
  self.import_white_list = import_white_list or list()
@@ -33,10 +33,11 @@ class DappierToolkit(BaseToolkit):
33
33
  (None, "DAPPIER_API_KEY"),
34
34
  ]
35
35
  )
36
- def __init__(self):
36
+ def __init__(self, timeout: Optional[float] = None):
37
37
  r"""Initialize the DappierTookit with API clients.The API keys and
38
38
  credentials are retrieved from environment variables.
39
39
  """
40
+ super().__init__(timeout=timeout)
40
41
  from dappier import Dappier
41
42
 
42
43
  dappier_api_key = os.environ.get("DAPPIER_API_KEY")