camel-ai 0.2.23a0__py3-none-any.whl → 0.2.24__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.

@@ -468,6 +468,42 @@ class RolePlaying:
468
468
 
469
469
  return init_msg
470
470
 
471
+ async def ainit_chat(
472
+ self, init_msg_content: Optional[str] = None
473
+ ) -> BaseMessage:
474
+ r"""Asynchronously initializes the chat by resetting both of the
475
+ assistant and user agents. Returns an initial message for the
476
+ role-playing session.
477
+
478
+ Args:
479
+ init_msg_content (str, optional): A user-specified initial message.
480
+ Will be sent to the role-playing session as the initial
481
+ message. (default: :obj:`None`)
482
+
483
+ Returns:
484
+ BaseMessage: A single `BaseMessage` representing the initial
485
+ message.
486
+ """
487
+ # Currently, reset() is synchronous, but if it becomes async in the
488
+ # future, we can await it here
489
+ self.assistant_agent.reset()
490
+ self.user_agent.reset()
491
+ default_init_msg_content = (
492
+ "Now start to give me instructions one by one. "
493
+ "Only reply with Instruction and Input."
494
+ )
495
+ if init_msg_content is None:
496
+ init_msg_content = default_init_msg_content
497
+
498
+ # Initialize a message sent by the assistant
499
+ init_msg = BaseMessage.make_assistant_message(
500
+ role_name=getattr(self.assistant_sys_msg, 'role_name', None)
501
+ or "assistant",
502
+ content=init_msg_content,
503
+ )
504
+
505
+ return init_msg
506
+
471
507
  def step(
472
508
  self,
473
509
  assistant_msg: BaseMessage,
@@ -549,3 +585,86 @@ class RolePlaying:
549
585
  info=user_response.info,
550
586
  ),
551
587
  )
588
+
589
+ async def astep(
590
+ self,
591
+ assistant_msg: BaseMessage,
592
+ ) -> Tuple[ChatAgentResponse, ChatAgentResponse]:
593
+ r"""Asynchronously advances the conversation by taking a message from
594
+ the assistant, processing it using the user agent, and then processing
595
+ the resulting message using the assistant agent. Returns a tuple
596
+ containing the resulting assistant message, whether the assistant
597
+ agent terminated the conversation, and any additional assistant
598
+ information, as well as a tuple containing the resulting user message,
599
+ whether the user agent terminated the conversation, and any additional
600
+ user information.
601
+
602
+ Args:
603
+ assistant_msg: A `BaseMessage` representing the message from the
604
+ assistant.
605
+
606
+ Returns:
607
+ Tuple[ChatAgentResponse, ChatAgentResponse]: A tuple containing two
608
+ ChatAgentResponse: the first struct contains the resulting
609
+ assistant message, whether the assistant agent terminated the
610
+ conversation, and any additional assistant information; the
611
+ second struct contains the resulting user message, whether the
612
+ user agent terminated the conversation, and any additional user
613
+ information.
614
+ """
615
+ user_response = await self.user_agent.astep(assistant_msg)
616
+ if user_response.terminated or user_response.msgs is None:
617
+ return (
618
+ ChatAgentResponse(msgs=[], terminated=False, info={}),
619
+ ChatAgentResponse(
620
+ msgs=[],
621
+ terminated=user_response.terminated,
622
+ info=user_response.info,
623
+ ),
624
+ )
625
+ user_msg = self._reduce_message_options(user_response.msgs)
626
+
627
+ # To prevent recording the same memory more than once (once in chat
628
+ # step and once in role play), and the model generates only one
629
+ # response when multi-response support is enabled.
630
+ if (
631
+ 'n' in self.user_agent.model_backend.model_config_dict.keys()
632
+ and self.user_agent.model_backend.model_config_dict['n'] > 1
633
+ ):
634
+ self.user_agent.record_message(user_msg)
635
+
636
+ assistant_response = await self.assistant_agent.astep(user_msg)
637
+ if assistant_response.terminated or assistant_response.msgs is None:
638
+ return (
639
+ ChatAgentResponse(
640
+ msgs=[],
641
+ terminated=assistant_response.terminated,
642
+ info=assistant_response.info,
643
+ ),
644
+ ChatAgentResponse(
645
+ msgs=[user_msg], terminated=False, info=user_response.info
646
+ ),
647
+ )
648
+ assistant_msg = self._reduce_message_options(assistant_response.msgs)
649
+
650
+ # To prevent recording the same memory more than once (once in chat
651
+ # step and once in role play), and the model generates only one
652
+ # response when multi-response support is enabled.
653
+ if (
654
+ 'n' in self.assistant_agent.model_backend.model_config_dict.keys()
655
+ and self.assistant_agent.model_backend.model_config_dict['n'] > 1
656
+ ):
657
+ self.assistant_agent.record_message(assistant_msg)
658
+
659
+ return (
660
+ ChatAgentResponse(
661
+ msgs=[assistant_msg],
662
+ terminated=assistant_response.terminated,
663
+ info=assistant_response.info,
664
+ ),
665
+ ChatAgentResponse(
666
+ msgs=[user_msg],
667
+ terminated=user_response.terminated,
668
+ info=user_response.info,
669
+ ),
670
+ )
@@ -43,13 +43,21 @@ 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
48
  from .networkx_toolkit import NetworkXToolkit
49
49
  from .semantic_scholar_toolkit import SemanticScholarToolkit
50
50
  from .zapier_toolkit import ZapierToolkit
51
51
  from .sympy_toolkit import SymPyToolkit
52
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 .mcp_toolkit import MCPToolkit
58
+ from .web_toolkit import WebToolkit
59
+ from .file_write_toolkit import FileWriteToolkit
60
+ from .terminal_toolkit import TerminalToolkit
53
61
 
54
62
 
55
63
  __all__ = [
@@ -88,4 +96,12 @@ __all__ = [
88
96
  'ZapierToolkit',
89
97
  'SymPyToolkit',
90
98
  'MinerUToolkit',
99
+ 'MCPToolkit',
100
+ 'AudioAnalysisToolkit',
101
+ 'ExcelToolkit',
102
+ 'VideoAnalysisToolkit',
103
+ 'ImageAnalysisToolkit',
104
+ 'WebToolkit',
105
+ 'FileWriteToolkit',
106
+ 'TerminalToolkit',
91
107
  ]
@@ -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
+ ]
@@ -0,0 +1,172 @@
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
+
15
+ from typing import List
16
+
17
+ import pandas as pd
18
+
19
+ from camel.logger import get_logger
20
+ from camel.toolkits.base import BaseToolkit
21
+ from camel.toolkits.function_tool import FunctionTool
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class ExcelToolkit(BaseToolkit):
27
+ r"""A class representing a toolkit for extract detailed cell information
28
+ from an Excel file.
29
+
30
+ This class provides method for processing docx, pdf, pptx, etc. It cannot
31
+ process excel files.
32
+ """
33
+
34
+ def _convert_to_markdown(self, df: pd.DataFrame) -> str:
35
+ r"""Convert DataFrame to Markdown format table.
36
+
37
+ Args:
38
+ df (pd.DataFrame): DataFrame containing the Excel data.
39
+
40
+ Returns:
41
+ str: Markdown formatted table.
42
+ """
43
+ from tabulate import tabulate
44
+
45
+ md_table = tabulate(df, headers='keys', tablefmt='pipe')
46
+ return str(md_table)
47
+
48
+ def extract_excel_content(self, document_path: str) -> str:
49
+ r"""Extract detailed cell information from an Excel file, including
50
+ multiple sheets.
51
+
52
+ Args:
53
+ document_path (str): The path of the Excel file.
54
+
55
+ Returns:
56
+ str: Extracted excel information, including details of each sheet.
57
+ """
58
+ from openpyxl import load_workbook
59
+ from xls2xlsx import XLS2XLSX
60
+
61
+ logger.debug(
62
+ f"Calling extract_excel_content with document_path"
63
+ f": {document_path}"
64
+ )
65
+
66
+ if not (
67
+ document_path.endswith("xls")
68
+ or document_path.endswith("xlsx")
69
+ or document_path.endswith("csv")
70
+ ):
71
+ logger.error("Only xls, xlsx, csv files are supported.")
72
+ return (
73
+ f"Failed to process file {document_path}: "
74
+ f"It is not excel format. Please try other ways."
75
+ )
76
+
77
+ if document_path.endswith("csv"):
78
+ try:
79
+ df = pd.read_csv(document_path)
80
+ md_table = self._convert_to_markdown(df)
81
+ return f"CSV File Processed:\n{md_table}"
82
+ except Exception as e:
83
+ logger.error(f"Failed to process file {document_path}: {e}")
84
+ return f"Failed to process file {document_path}: {e}"
85
+
86
+ if document_path.endswith("xls"):
87
+ output_path = document_path.replace(".xls", ".xlsx")
88
+ x2x = XLS2XLSX(document_path)
89
+ x2x.to_xlsx(output_path)
90
+ document_path = output_path
91
+
92
+ # Load the Excel workbook
93
+ wb = load_workbook(document_path, data_only=True)
94
+ sheet_info_list = []
95
+
96
+ # Iterate through all sheets
97
+ for sheet in wb.sheetnames:
98
+ ws = wb[sheet]
99
+ cell_info_list = []
100
+
101
+ for row in ws.iter_rows():
102
+ for cell in row:
103
+ row_num = cell.row
104
+ col_letter = cell.column_letter
105
+
106
+ cell_value = cell.value
107
+
108
+ font_color = None
109
+ if (
110
+ cell.font
111
+ and cell.font.color
112
+ and "rgb=None" not in str(cell.font.color)
113
+ ): # Handle font color
114
+ font_color = cell.font.color.rgb
115
+
116
+ fill_color = None
117
+ if (
118
+ cell.fill
119
+ and cell.fill.fgColor
120
+ and "rgb=None" not in str(cell.fill.fgColor)
121
+ ): # Handle fill color
122
+ fill_color = cell.fill.fgColor.rgb
123
+
124
+ cell_info_list.append(
125
+ {
126
+ "index": f"{row_num}{col_letter}",
127
+ "value": cell_value,
128
+ "font_color": font_color,
129
+ "fill_color": fill_color,
130
+ }
131
+ )
132
+
133
+ # Convert the sheet to a DataFrame and then to markdown
134
+ sheet_df = pd.read_excel(
135
+ document_path, sheet_name=sheet, engine='openpyxl'
136
+ )
137
+ markdown_content = self._convert_to_markdown(sheet_df)
138
+
139
+ # Collect all information for the sheet
140
+ sheet_info = {
141
+ "sheet_name": sheet,
142
+ "cell_info_list": cell_info_list,
143
+ "markdown_content": markdown_content,
144
+ }
145
+ sheet_info_list.append(sheet_info)
146
+
147
+ result_str = ""
148
+ for sheet_info in sheet_info_list:
149
+ result_str += f"""
150
+ Sheet Name: {sheet_info['sheet_name']}
151
+ Cell information list:
152
+ {sheet_info['cell_info_list']}
153
+
154
+ Markdown View of the content:
155
+ {sheet_info['markdown_content']}
156
+
157
+ {'-'*40}
158
+ """
159
+
160
+ return result_str
161
+
162
+ def get_tools(self) -> List[FunctionTool]:
163
+ r"""Returns a list of FunctionTool objects representing the functions
164
+ in the toolkit.
165
+
166
+ Returns:
167
+ List[FunctionTool]: A list of FunctionTool objects representing
168
+ the functions in the toolkit.
169
+ """
170
+ return [
171
+ FunctionTool(self.extract_excel_content),
172
+ ]