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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +16 -2
- camel/configs/anthropic_config.py +45 -11
- camel/datagen/self_improving_cot.py +2 -2
- camel/datagen/self_instruct/self_instruct.py +46 -2
- camel/models/__init__.py +2 -0
- camel/models/anthropic_model.py +5 -1
- camel/models/base_audio_model.py +92 -0
- camel/models/fish_audio_model.py +18 -8
- camel/models/model_manager.py +9 -0
- camel/models/openai_audio_models.py +80 -1
- camel/societies/role_playing.py +119 -0
- camel/toolkits/__init__.py +17 -1
- camel/toolkits/audio_analysis_toolkit.py +238 -0
- camel/toolkits/excel_toolkit.py +172 -0
- camel/toolkits/file_write_toolkit.py +371 -0
- camel/toolkits/image_analysis_toolkit.py +202 -0
- camel/toolkits/mcp_toolkit.py +251 -0
- camel/toolkits/page_script.js +376 -0
- camel/toolkits/terminal_toolkit.py +421 -0
- camel/toolkits/video_analysis_toolkit.py +407 -0
- camel/toolkits/{video_toolkit.py → video_download_toolkit.py} +19 -25
- camel/toolkits/web_toolkit.py +1306 -0
- camel/types/enums.py +3 -0
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/METADATA +241 -106
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/RECORD +57 -47
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.23a0.dist-info → camel_ai-0.2.24.dist-info/licenses}/LICENSE +0 -0
camel/societies/role_playing.py
CHANGED
|
@@ -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
|
+
)
|
camel/toolkits/__init__.py
CHANGED
|
@@ -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 .
|
|
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
|
+
]
|