camel-ai 0.2.41__py3-none-any.whl → 0.2.42__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 +24 -4
- camel/environments/single_step.py +28 -11
- camel/models/vllm_model.py +16 -0
- camel/toolkits/terminal_toolkit.py +729 -115
- {camel_ai-0.2.41.dist-info → camel_ai-0.2.42.dist-info}/METADATA +1 -1
- {camel_ai-0.2.41.dist-info → camel_ai-0.2.42.dist-info}/RECORD +9 -9
- {camel_ai-0.2.41.dist-info → camel_ai-0.2.42.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.41.dist-info → camel_ai-0.2.42.dist-info}/licenses/LICENSE +0 -0
camel/__init__.py
CHANGED
camel/agents/chat_agent.py
CHANGED
|
@@ -327,7 +327,10 @@ class ChatAgent(BaseAgent):
|
|
|
327
327
|
return False
|
|
328
328
|
|
|
329
329
|
def update_memory(
|
|
330
|
-
self,
|
|
330
|
+
self,
|
|
331
|
+
message: BaseMessage,
|
|
332
|
+
role: OpenAIBackendRole,
|
|
333
|
+
timestamp: Optional[float] = None,
|
|
331
334
|
) -> None:
|
|
332
335
|
r"""Updates the agent memory with a new message.
|
|
333
336
|
|
|
@@ -335,12 +338,19 @@ class ChatAgent(BaseAgent):
|
|
|
335
338
|
message (BaseMessage): The new message to add to the stored
|
|
336
339
|
messages.
|
|
337
340
|
role (OpenAIBackendRole): The backend role type.
|
|
341
|
+
timestamp (Optional[float], optional): Custom timestamp for the
|
|
342
|
+
memory record. If None, current timestamp will be used.
|
|
343
|
+
(default: :obj:`None`)
|
|
338
344
|
"""
|
|
345
|
+
from datetime import timezone
|
|
346
|
+
|
|
339
347
|
self.memory.write_record(
|
|
340
348
|
MemoryRecord(
|
|
341
349
|
message=message,
|
|
342
350
|
role_at_backend=role,
|
|
343
|
-
timestamp=
|
|
351
|
+
timestamp=timestamp
|
|
352
|
+
if timestamp is not None
|
|
353
|
+
else datetime.now(timezone.utc).timestamp(),
|
|
344
354
|
agent_id=self.agent_id,
|
|
345
355
|
)
|
|
346
356
|
)
|
|
@@ -1331,8 +1341,18 @@ class ChatAgent(BaseAgent):
|
|
|
1331
1341
|
tool_call_id=tool_call_id,
|
|
1332
1342
|
)
|
|
1333
1343
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1344
|
+
# Use slightly different timestamps to ensure correct ordering
|
|
1345
|
+
# This ensures the assistant message (tool call) always appears before
|
|
1346
|
+
# the function message (tool result) in the conversation context
|
|
1347
|
+
current_time = datetime.now().timestamp()
|
|
1348
|
+
self.update_memory(
|
|
1349
|
+
assist_msg, OpenAIBackendRole.ASSISTANT, timestamp=current_time
|
|
1350
|
+
)
|
|
1351
|
+
self.update_memory(
|
|
1352
|
+
func_msg,
|
|
1353
|
+
OpenAIBackendRole.FUNCTION,
|
|
1354
|
+
timestamp=current_time + 0.001,
|
|
1355
|
+
)
|
|
1336
1356
|
|
|
1337
1357
|
# Record information about this tool call
|
|
1338
1358
|
tool_record = ToolCallingRecord(
|
|
@@ -224,7 +224,7 @@ class SingleStepEnv:
|
|
|
224
224
|
raise TypeError(f"Unsupported dataset type: {type(self.dataset)}")
|
|
225
225
|
|
|
226
226
|
async def step(
|
|
227
|
-
self, action: Union[Action, List[Action], str]
|
|
227
|
+
self, action: Union[Action, List[Action], str, Dict[int, str]]
|
|
228
228
|
) -> Union[
|
|
229
229
|
Tuple[Observation, float, bool, Dict[str, Any]],
|
|
230
230
|
List[Tuple[Observation, float, bool, Dict[str, Any]]],
|
|
@@ -242,13 +242,15 @@ class SingleStepEnv:
|
|
|
242
242
|
the observation will not change.
|
|
243
243
|
|
|
244
244
|
Args:
|
|
245
|
-
action (Union[Action, List[Action], str]):
|
|
245
|
+
action (Union[Action, List[Action], str, Dict[int, str]]):
|
|
246
246
|
The action(s) taken by the agent,
|
|
247
247
|
which should contain the response(s)
|
|
248
248
|
to the observation(s). Can be:
|
|
249
249
|
- A single `Action` object (for batch size 1),
|
|
250
250
|
- A list of `Action` objects (for batched evaluation),
|
|
251
251
|
- A raw string (only allowed when batch size is 1).
|
|
252
|
+
- A dict that maps indices to their `llm_response`
|
|
253
|
+
(for batched evaluation)
|
|
252
254
|
|
|
253
255
|
Returns:
|
|
254
256
|
Union[Tuple[Observation, float, bool, Dict[str, Any]], List[...]]:
|
|
@@ -293,6 +295,7 @@ class SingleStepEnv:
|
|
|
293
295
|
f"total batch size ({self.current_batch_size})"
|
|
294
296
|
)
|
|
295
297
|
|
|
298
|
+
indices = [act.index for act in actions]
|
|
296
299
|
proposed_solutions = [act.llm_response for act in actions]
|
|
297
300
|
ground_truths: List[str] = []
|
|
298
301
|
for idx in indices:
|
|
@@ -334,21 +337,22 @@ class SingleStepEnv:
|
|
|
334
337
|
).as_tuple()
|
|
335
338
|
for i in range(len(actions))
|
|
336
339
|
]
|
|
340
|
+
|
|
337
341
|
for _, idx in enumerate(indices):
|
|
338
342
|
self._states_done[idx] = True
|
|
339
343
|
|
|
340
344
|
return step_results[0] if len(step_results) == 1 else step_results
|
|
341
345
|
|
|
342
346
|
def _normalize_actions(
|
|
343
|
-
self, action: Union[Action, List[Action], str]
|
|
347
|
+
self, action: Union[Action, List[Action], str, Dict[int, str]]
|
|
344
348
|
) -> List[Action]:
|
|
345
349
|
r"""Normalize the user-provided action(s) into a validated list
|
|
346
350
|
of `Action` objects.
|
|
347
351
|
|
|
348
352
|
This method handles flexibility in input format by converting
|
|
349
|
-
raw strings (only allowed when batch size is 1) and
|
|
350
|
-
all necessary structure and integrity checks on
|
|
351
|
-
(e.g., index bounds, duplicates).
|
|
353
|
+
raw strings (only allowed when batch size is 1) and dictionaries,
|
|
354
|
+
ensuring all necessary structure and integrity checks on
|
|
355
|
+
actions (e.g., index bounds, duplicates).
|
|
352
356
|
|
|
353
357
|
Args:
|
|
354
358
|
action (Union[Action, List[Action], str]):
|
|
@@ -357,6 +361,7 @@ class SingleStepEnv:
|
|
|
357
361
|
- A list of `Action` objects.
|
|
358
362
|
- A raw string (if `batch_size == 1`), auto-wrapped
|
|
359
363
|
in an `Action`.
|
|
364
|
+
- A dict mapping int indices to str responses
|
|
360
365
|
|
|
361
366
|
Returns:
|
|
362
367
|
List[Action]: A list of validated `Action` instances
|
|
@@ -368,8 +373,9 @@ class SingleStepEnv:
|
|
|
368
373
|
- Action list is empty,
|
|
369
374
|
- Index mismatches expected values
|
|
370
375
|
(e.g., 0 for batch size 1),
|
|
371
|
-
- Wrong structure is used
|
|
372
|
-
|
|
376
|
+
- Wrong structure is used (e.g.,
|
|
377
|
+
string used with batch size > 1,
|
|
378
|
+
dict used with batch size == 1).
|
|
373
379
|
TypeError: If the action is of an unsupported type.
|
|
374
380
|
"""
|
|
375
381
|
|
|
@@ -380,9 +386,20 @@ class SingleStepEnv:
|
|
|
380
386
|
" when batch_size == 1"
|
|
381
387
|
)
|
|
382
388
|
logger.warning("Auto-converting from str to Action", stacklevel=2)
|
|
383
|
-
|
|
389
|
+
actions = [Action(index=0, llm_response=action)]
|
|
390
|
+
|
|
391
|
+
elif isinstance(action, dict):
|
|
392
|
+
if not all(isinstance(k, int) for k in action.keys()):
|
|
393
|
+
raise ValueError("All dictionary keys must be integers")
|
|
384
394
|
|
|
385
|
-
|
|
395
|
+
if self.current_batch_size == 1 and list(action.keys()) != [0]:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
"For batch_size=1, dict input must have exactly one key: 0"
|
|
398
|
+
)
|
|
399
|
+
actions = [
|
|
400
|
+
Action(index=k, llm_response=v) for k, v in action.items()
|
|
401
|
+
]
|
|
402
|
+
elif isinstance(action, Action):
|
|
386
403
|
actions = [action]
|
|
387
404
|
elif isinstance(action, list):
|
|
388
405
|
if not action:
|
|
@@ -397,7 +414,7 @@ class SingleStepEnv:
|
|
|
397
414
|
|
|
398
415
|
if self.current_batch_size == 1 and len(actions) != 1:
|
|
399
416
|
raise ValueError(
|
|
400
|
-
"For batch_size=1, expect a single Action or a "
|
|
417
|
+
"For batch_size=1, expect a single Action, a dictionary or a "
|
|
401
418
|
"list containing exactly one Action"
|
|
402
419
|
)
|
|
403
420
|
|
camel/models/vllm_model.py
CHANGED
|
@@ -162,6 +162,14 @@ class VLLMModel(BaseModelBackend):
|
|
|
162
162
|
if response_format:
|
|
163
163
|
kwargs["response_format"] = {"type": "json_object"}
|
|
164
164
|
|
|
165
|
+
# Remove additionalProperties from each tool's function parameters
|
|
166
|
+
if tools and "tools" in kwargs:
|
|
167
|
+
for tool in kwargs["tools"]:
|
|
168
|
+
if "function" in tool and "parameters" in tool["function"]:
|
|
169
|
+
tool["function"]["parameters"].pop(
|
|
170
|
+
"additionalProperties", None
|
|
171
|
+
)
|
|
172
|
+
|
|
165
173
|
response = await self._async_client.chat.completions.create(
|
|
166
174
|
messages=messages,
|
|
167
175
|
model=self.model_type,
|
|
@@ -197,6 +205,14 @@ class VLLMModel(BaseModelBackend):
|
|
|
197
205
|
if response_format:
|
|
198
206
|
kwargs["response_format"] = {"type": "json_object"}
|
|
199
207
|
|
|
208
|
+
# Remove additionalProperties from each tool's function parameters
|
|
209
|
+
if tools and "tools" in kwargs:
|
|
210
|
+
for tool in kwargs["tools"]:
|
|
211
|
+
if "function" in tool and "parameters" in tool["function"]:
|
|
212
|
+
tool["function"]["parameters"].pop(
|
|
213
|
+
"additionalProperties", None
|
|
214
|
+
)
|
|
215
|
+
|
|
200
216
|
response = self._client.chat.completions.create(
|
|
201
217
|
messages=messages,
|
|
202
218
|
model=self.model_type,
|
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
|
|
15
|
+
import atexit
|
|
15
16
|
import os
|
|
17
|
+
import platform
|
|
18
|
+
import queue
|
|
16
19
|
import subprocess
|
|
17
|
-
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
import venv
|
|
23
|
+
from queue import Queue
|
|
24
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
25
|
|
|
19
26
|
from camel.logger import get_logger
|
|
20
27
|
from camel.toolkits.base import BaseToolkit
|
|
@@ -36,7 +43,19 @@ class TerminalToolkit(BaseToolkit):
|
|
|
36
43
|
timeout (Optional[float]): The timeout for terminal operations.
|
|
37
44
|
shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
|
|
38
45
|
shell session information. If None, an empty dictionary will be
|
|
39
|
-
used.
|
|
46
|
+
used. (default: :obj:`{}`)
|
|
47
|
+
working_dir (str): The working directory for operations.
|
|
48
|
+
If specified, all execution and write operations will be restricted
|
|
49
|
+
to this directory. Read operations can access paths outside this
|
|
50
|
+
directory.(default: :obj:`"./workspace"`)
|
|
51
|
+
need_terminal (bool): Whether to create a terminal interface.
|
|
52
|
+
(default: :obj:`True`)
|
|
53
|
+
use_shell_mode (bool): Whether to use shell mode for command execution.
|
|
54
|
+
(default: :obj:`True`)
|
|
55
|
+
clone_current_env (bool): Whether to clone the current Python
|
|
56
|
+
environment.(default: :obj:`False`)
|
|
57
|
+
safe_mode (bool): Whether to enable safe mode to restrict operations.
|
|
58
|
+
(default: :obj:`True`)
|
|
40
59
|
|
|
41
60
|
Note:
|
|
42
61
|
Most functions are compatible with Unix-based systems (macOS, Linux).
|
|
@@ -48,14 +67,260 @@ class TerminalToolkit(BaseToolkit):
|
|
|
48
67
|
self,
|
|
49
68
|
timeout: Optional[float] = None,
|
|
50
69
|
shell_sessions: Optional[Dict[str, Any]] = None,
|
|
70
|
+
working_dir: str = "./workspace",
|
|
71
|
+
need_terminal: bool = True,
|
|
72
|
+
use_shell_mode: bool = True,
|
|
73
|
+
clone_current_env: bool = False,
|
|
74
|
+
safe_mode: bool = True,
|
|
51
75
|
):
|
|
52
|
-
import platform
|
|
53
|
-
|
|
54
76
|
super().__init__(timeout=timeout)
|
|
55
77
|
self.shell_sessions = shell_sessions or {}
|
|
56
|
-
self.os_type = (
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
self.os_type = platform.system()
|
|
79
|
+
self.output_queue: Queue[str] = Queue()
|
|
80
|
+
self.agent_queue: Queue[str] = Queue()
|
|
81
|
+
self.terminal_ready = threading.Event()
|
|
82
|
+
self.gui_thread = None
|
|
83
|
+
self.safe_mode = safe_mode
|
|
84
|
+
|
|
85
|
+
self.cloned_env_path = None
|
|
86
|
+
self.use_shell_mode = use_shell_mode
|
|
87
|
+
|
|
88
|
+
self.python_executable = sys.executable
|
|
89
|
+
self.is_macos = platform.system() == 'Darwin'
|
|
90
|
+
|
|
91
|
+
atexit.register(self.__del__)
|
|
92
|
+
|
|
93
|
+
if not os.path.exists(working_dir):
|
|
94
|
+
os.makedirs(working_dir, exist_ok=True)
|
|
95
|
+
self.working_dir = os.path.abspath(working_dir)
|
|
96
|
+
self._update_terminal_output(
|
|
97
|
+
f"Working directory set to: {self.working_dir}\n"
|
|
98
|
+
)
|
|
99
|
+
if self.safe_mode:
|
|
100
|
+
self._update_terminal_output(
|
|
101
|
+
"Safe mode enabled: Write operations can only "
|
|
102
|
+
"be performed within the working directory\n"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if clone_current_env:
|
|
106
|
+
self.cloned_env_path = os.path.join(self.working_dir, ".venv")
|
|
107
|
+
self._clone_current_environment()
|
|
108
|
+
else:
|
|
109
|
+
self.cloned_env_path = None
|
|
110
|
+
|
|
111
|
+
if need_terminal:
|
|
112
|
+
if self.is_macos:
|
|
113
|
+
# macOS uses non-GUI mode
|
|
114
|
+
logger.info("Detected macOS environment, using non-GUI mode")
|
|
115
|
+
self._setup_file_output()
|
|
116
|
+
self.terminal_ready.set()
|
|
117
|
+
else:
|
|
118
|
+
# Other platforms use normal GUI
|
|
119
|
+
self.gui_thread = threading.Thread(
|
|
120
|
+
target=self._create_terminal, daemon=True
|
|
121
|
+
)
|
|
122
|
+
self.gui_thread.start()
|
|
123
|
+
self.terminal_ready.wait(timeout=5)
|
|
124
|
+
|
|
125
|
+
def _setup_file_output(self):
|
|
126
|
+
r"""Set up file output to replace GUI, using a fixed file to simulate
|
|
127
|
+
terminal.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
self.log_file = os.path.join(os.getcwd(), "camel_terminal.txt")
|
|
131
|
+
|
|
132
|
+
if os.path.exists(self.log_file):
|
|
133
|
+
with open(self.log_file, "w") as f:
|
|
134
|
+
f.truncate(0)
|
|
135
|
+
f.write("CAMEL Terminal Session\n")
|
|
136
|
+
f.write("=" * 50 + "\n")
|
|
137
|
+
f.write(f"Working Directory: {os.getcwd()}\n")
|
|
138
|
+
f.write("=" * 50 + "\n\n")
|
|
139
|
+
else:
|
|
140
|
+
with open(self.log_file, "w") as f:
|
|
141
|
+
f.write("CAMEL Terminal Session\n")
|
|
142
|
+
f.write("=" * 50 + "\n")
|
|
143
|
+
f.write(f"Working Directory: {os.getcwd()}\n")
|
|
144
|
+
f.write("=" * 50 + "\n\n")
|
|
145
|
+
|
|
146
|
+
# Inform the user
|
|
147
|
+
logger.info(f"Terminal output redirected to: {self.log_file}")
|
|
148
|
+
|
|
149
|
+
def file_update(output: str):
|
|
150
|
+
try:
|
|
151
|
+
# Directly append to the end of the file
|
|
152
|
+
with open(self.log_file, "a") as f:
|
|
153
|
+
f.write(output)
|
|
154
|
+
# If the output does not end with a newline, add one
|
|
155
|
+
if output and not output.endswith('\n'):
|
|
156
|
+
f.write('\n')
|
|
157
|
+
# Ensure the agent also receives the output
|
|
158
|
+
self.agent_queue.put(output)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Failed to write to terminal: {e}")
|
|
161
|
+
|
|
162
|
+
# Replace the update method
|
|
163
|
+
self._update_terminal_output = file_update
|
|
164
|
+
|
|
165
|
+
def _clone_current_environment(self):
|
|
166
|
+
r"""Create a new Python virtual environment."""
|
|
167
|
+
try:
|
|
168
|
+
if os.path.exists(self.cloned_env_path):
|
|
169
|
+
self._update_terminal_output(
|
|
170
|
+
f"Using existing environment: {self.cloned_env_path}\n"
|
|
171
|
+
)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
self._update_terminal_output(
|
|
175
|
+
f"Creating new Python environment at:{self.cloned_env_path}\n"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
venv.create(self.cloned_env_path, with_pip=True)
|
|
179
|
+
self._update_terminal_output(
|
|
180
|
+
"New Python environment created successfully!\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self._update_terminal_output(
|
|
185
|
+
f"Failed to create environment: {e!s}\n"
|
|
186
|
+
)
|
|
187
|
+
logger.error(f"Failed to create environment: {e}")
|
|
188
|
+
|
|
189
|
+
def _create_terminal(self):
|
|
190
|
+
r"""Create a terminal GUI."""
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
import tkinter as tk
|
|
194
|
+
from tkinter import scrolledtext
|
|
195
|
+
|
|
196
|
+
def update_terminal():
|
|
197
|
+
try:
|
|
198
|
+
while True:
|
|
199
|
+
output = self.output_queue.get_nowait()
|
|
200
|
+
if isinstance(output, bytes):
|
|
201
|
+
output = output.decode('utf-8', errors='replace')
|
|
202
|
+
self.terminal.insert(tk.END, output)
|
|
203
|
+
self.terminal.see(tk.END)
|
|
204
|
+
except queue.Empty:
|
|
205
|
+
if hasattr(self, 'root') and self.root:
|
|
206
|
+
self.root.after(100, update_terminal)
|
|
207
|
+
|
|
208
|
+
self.root = tk.Tk()
|
|
209
|
+
self.root.title(f"{self.os_type} Terminal")
|
|
210
|
+
|
|
211
|
+
self.root.geometry("800x600")
|
|
212
|
+
self.root.minsize(400, 300)
|
|
213
|
+
|
|
214
|
+
self.terminal = scrolledtext.ScrolledText(
|
|
215
|
+
self.root,
|
|
216
|
+
wrap=tk.WORD,
|
|
217
|
+
bg='black',
|
|
218
|
+
fg='white',
|
|
219
|
+
font=('Consolas', 10),
|
|
220
|
+
insertbackground='white', # Cursor color
|
|
221
|
+
)
|
|
222
|
+
self.terminal.pack(fill=tk.BOTH, expand=True)
|
|
223
|
+
|
|
224
|
+
# Set the handling for closing the window
|
|
225
|
+
def on_closing():
|
|
226
|
+
self.root.quit()
|
|
227
|
+
self.root.destroy()
|
|
228
|
+
self.root = None
|
|
229
|
+
|
|
230
|
+
self.root.protocol("WM_DELETE_WINDOW", on_closing)
|
|
231
|
+
|
|
232
|
+
# Start updating
|
|
233
|
+
update_terminal()
|
|
234
|
+
|
|
235
|
+
# Mark the terminal as ready
|
|
236
|
+
self.terminal_ready.set()
|
|
237
|
+
|
|
238
|
+
# Start the main loop
|
|
239
|
+
self.root.mainloop()
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to create terminal: {e}")
|
|
243
|
+
self.terminal_ready.set()
|
|
244
|
+
|
|
245
|
+
def _update_terminal_output(self, output: str):
|
|
246
|
+
r"""Update terminal output and send to agent.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
output (str): The output to be sent to the agent
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
# If it is macOS , only write to file
|
|
253
|
+
if self.is_macos:
|
|
254
|
+
if hasattr(self, 'log_file'):
|
|
255
|
+
with open(self.log_file, "a") as f:
|
|
256
|
+
f.write(output)
|
|
257
|
+
# Ensure the agent also receives the output
|
|
258
|
+
self.agent_queue.put(output)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# For other cases, try to update the GUI (if it exists)
|
|
262
|
+
if hasattr(self, 'root') and self.root:
|
|
263
|
+
self.output_queue.put(output)
|
|
264
|
+
|
|
265
|
+
# Always send to agent queue
|
|
266
|
+
self.agent_queue.put(output)
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Failed to update terminal output: {e}")
|
|
270
|
+
|
|
271
|
+
def _is_path_within_working_dir(self, path: str) -> bool:
|
|
272
|
+
r"""Check if the path is within the working directory.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
path (str): The path to check
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
bool: Returns True if the path is within the working directory,
|
|
279
|
+
otherwise returns False
|
|
280
|
+
"""
|
|
281
|
+
abs_path = os.path.abspath(path)
|
|
282
|
+
return abs_path.startswith(self.working_dir)
|
|
283
|
+
|
|
284
|
+
def _enforce_working_dir_for_execution(self, path: str) -> Optional[str]:
|
|
285
|
+
r"""Enforce working directory restrictions, return error message
|
|
286
|
+
if execution path is not within the working directory.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
path (str): The path to be used for executing operations
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Optional[str]: Returns error message if the path is not within
|
|
293
|
+
the working directory, otherwise returns None
|
|
294
|
+
"""
|
|
295
|
+
if not self._is_path_within_working_dir(path):
|
|
296
|
+
return (
|
|
297
|
+
f"Operation restriction: Execution path {path} must "
|
|
298
|
+
f"be within working directory {self.working_dir}"
|
|
299
|
+
)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _copy_external_file_to_workdir(
|
|
303
|
+
self, external_file: str
|
|
304
|
+
) -> Optional[str]:
|
|
305
|
+
r"""Copy external file to working directory.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
external_file (str): The path of the external file
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Optional[str]: New path after copying to the working directory,
|
|
312
|
+
returns None on failure
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
import shutil
|
|
316
|
+
|
|
317
|
+
filename = os.path.basename(external_file)
|
|
318
|
+
new_path = os.path.join(self.working_dir, filename)
|
|
319
|
+
shutil.copy2(external_file, new_path)
|
|
320
|
+
return new_path
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Failed to copy file: {e}")
|
|
323
|
+
return None
|
|
59
324
|
|
|
60
325
|
def file_find_in_content(
|
|
61
326
|
self, file: str, regex: str, sudo: bool = False
|
|
@@ -72,6 +337,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
72
337
|
Returns:
|
|
73
338
|
str: Matching content found in the file.
|
|
74
339
|
"""
|
|
340
|
+
|
|
75
341
|
if not os.path.exists(file):
|
|
76
342
|
return f"File not found: {file}"
|
|
77
343
|
|
|
@@ -80,6 +346,9 @@ class TerminalToolkit(BaseToolkit):
|
|
|
80
346
|
|
|
81
347
|
command = []
|
|
82
348
|
if sudo:
|
|
349
|
+
error_msg = self._enforce_working_dir_for_execution(file)
|
|
350
|
+
if error_msg:
|
|
351
|
+
return error_msg
|
|
83
352
|
command.extend(["sudo"])
|
|
84
353
|
|
|
85
354
|
if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
|
|
@@ -119,38 +388,283 @@ class TerminalToolkit(BaseToolkit):
|
|
|
119
388
|
else: # Windows
|
|
120
389
|
# For Windows, we use dir command with /s for recursive search
|
|
121
390
|
# and /b for bare format
|
|
391
|
+
|
|
122
392
|
pattern = glob
|
|
123
393
|
file_path = os.path.join(path, pattern).replace('/', '\\')
|
|
124
394
|
command.extend(["cmd", "/c", "dir", "/s", "/b", file_path])
|
|
125
395
|
|
|
126
396
|
try:
|
|
127
397
|
result = subprocess.run(
|
|
128
|
-
command,
|
|
398
|
+
command,
|
|
399
|
+
check=False,
|
|
400
|
+
capture_output=True,
|
|
401
|
+
text=True,
|
|
402
|
+
shell=False,
|
|
129
403
|
)
|
|
130
|
-
|
|
404
|
+
|
|
405
|
+
output = result.stdout.strip()
|
|
406
|
+
if self.os_type == 'Windows':
|
|
407
|
+
output = output.replace('\\', '/')
|
|
408
|
+
return output
|
|
131
409
|
except subprocess.SubprocessError as e:
|
|
132
410
|
logger.error(f"Error finding files by name: {e}")
|
|
133
411
|
return f"Error: {e!s}"
|
|
134
412
|
|
|
135
|
-
def
|
|
136
|
-
r"""
|
|
413
|
+
def _sanitize_command(self, command: str, exec_dir: str) -> Tuple:
|
|
414
|
+
r"""Check and modify command to ensure safety.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
command (str): The command to check
|
|
418
|
+
exec_dir (str): The directory to execute the command in
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Tuple: (is safe, modified command or error message)
|
|
422
|
+
"""
|
|
423
|
+
if not self.safe_mode:
|
|
424
|
+
return True, command
|
|
425
|
+
|
|
426
|
+
if not command or command.strip() == "":
|
|
427
|
+
return False, "Empty command"
|
|
428
|
+
|
|
429
|
+
# Use shlex for safer command parsing
|
|
430
|
+
import shlex
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
parts = shlex.split(command)
|
|
434
|
+
except ValueError as e:
|
|
435
|
+
# Handle malformed commands (e.g., unbalanced quotes)
|
|
436
|
+
return False, f"Invalid command format: {e}"
|
|
437
|
+
|
|
438
|
+
if not parts:
|
|
439
|
+
return False, "Empty command"
|
|
440
|
+
|
|
441
|
+
# Get base command
|
|
442
|
+
base_cmd = parts[0].lower()
|
|
443
|
+
|
|
444
|
+
# Handle special commands
|
|
445
|
+
if base_cmd in ['cd', 'chdir']:
|
|
446
|
+
# Check if cd command attempts to leave the working directory
|
|
447
|
+
if len(parts) > 1:
|
|
448
|
+
target_dir = parts[1].strip('"\'')
|
|
449
|
+
if (
|
|
450
|
+
target_dir.startswith('/')
|
|
451
|
+
or target_dir.startswith('\\')
|
|
452
|
+
or ':' in target_dir
|
|
453
|
+
):
|
|
454
|
+
# Absolute path
|
|
455
|
+
abs_path = os.path.abspath(target_dir)
|
|
456
|
+
else:
|
|
457
|
+
# Relative path
|
|
458
|
+
abs_path = os.path.abspath(
|
|
459
|
+
os.path.join(exec_dir, target_dir)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if not self._is_path_within_working_dir(abs_path):
|
|
463
|
+
return False, (
|
|
464
|
+
f"Safety restriction: Cannot change to directory "
|
|
465
|
+
f"outside of working directory {self.working_dir}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Check file operation commands
|
|
469
|
+
elif base_cmd in [
|
|
470
|
+
'rm',
|
|
471
|
+
'del',
|
|
472
|
+
'rmdir',
|
|
473
|
+
'rd',
|
|
474
|
+
'deltree',
|
|
475
|
+
'erase',
|
|
476
|
+
'unlink',
|
|
477
|
+
'shred',
|
|
478
|
+
'srm',
|
|
479
|
+
'wipe',
|
|
480
|
+
'remove',
|
|
481
|
+
]:
|
|
482
|
+
# Check targets of delete commands
|
|
483
|
+
for _, part in enumerate(parts[1:], 1):
|
|
484
|
+
if part.startswith('-') or part.startswith(
|
|
485
|
+
'/'
|
|
486
|
+
): # Skip options
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
target = part.strip('"\'')
|
|
490
|
+
if (
|
|
491
|
+
target.startswith('/')
|
|
492
|
+
or target.startswith('\\')
|
|
493
|
+
or ':' in target
|
|
494
|
+
):
|
|
495
|
+
# Absolute path
|
|
496
|
+
abs_path = os.path.abspath(target)
|
|
497
|
+
else:
|
|
498
|
+
# Relative path
|
|
499
|
+
abs_path = os.path.abspath(os.path.join(exec_dir, target))
|
|
500
|
+
|
|
501
|
+
if not self._is_path_within_working_dir(abs_path):
|
|
502
|
+
return False, (
|
|
503
|
+
f"Safety restriction: Cannot delete files outside "
|
|
504
|
+
f"of working directory {self.working_dir}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Check write/modify commands
|
|
508
|
+
elif base_cmd in [
|
|
509
|
+
'touch',
|
|
510
|
+
'mkdir',
|
|
511
|
+
'md',
|
|
512
|
+
'echo',
|
|
513
|
+
'cat',
|
|
514
|
+
'cp',
|
|
515
|
+
'copy',
|
|
516
|
+
'mv',
|
|
517
|
+
'move',
|
|
518
|
+
'rename',
|
|
519
|
+
'ren',
|
|
520
|
+
'write',
|
|
521
|
+
'output',
|
|
522
|
+
]:
|
|
523
|
+
# Check for redirection symbols
|
|
524
|
+
full_cmd = command.lower()
|
|
525
|
+
if '>' in full_cmd:
|
|
526
|
+
# Find the file path after redirection
|
|
527
|
+
redirect_parts = command.split('>')
|
|
528
|
+
if len(redirect_parts) > 1:
|
|
529
|
+
output_file = (
|
|
530
|
+
redirect_parts[1].strip().split()[0].strip('"\'')
|
|
531
|
+
)
|
|
532
|
+
if (
|
|
533
|
+
output_file.startswith('/')
|
|
534
|
+
or output_file.startswith('\\')
|
|
535
|
+
or ':' in output_file
|
|
536
|
+
):
|
|
537
|
+
# Absolute path
|
|
538
|
+
abs_path = os.path.abspath(output_file)
|
|
539
|
+
else:
|
|
540
|
+
# Relative path
|
|
541
|
+
abs_path = os.path.abspath(
|
|
542
|
+
os.path.join(exec_dir, output_file)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if not self._is_path_within_working_dir(abs_path):
|
|
546
|
+
return False, (
|
|
547
|
+
f"Safety restriction: Cannot write to file "
|
|
548
|
+
f"outside of working directory {self.working_dir}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# For cp/mv commands, check target paths
|
|
552
|
+
if base_cmd in ['cp', 'copy', 'mv', 'move']:
|
|
553
|
+
# Simple handling, assuming the last parameter is the target
|
|
554
|
+
if len(parts) > 2:
|
|
555
|
+
target = parts[-1].strip('"\'')
|
|
556
|
+
if (
|
|
557
|
+
target.startswith('/')
|
|
558
|
+
or target.startswith('\\')
|
|
559
|
+
or ':' in target
|
|
560
|
+
):
|
|
561
|
+
# Absolute path
|
|
562
|
+
abs_path = os.path.abspath(target)
|
|
563
|
+
else:
|
|
564
|
+
# Relative path
|
|
565
|
+
abs_path = os.path.abspath(
|
|
566
|
+
os.path.join(exec_dir, target)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
if not self._is_path_within_working_dir(abs_path):
|
|
570
|
+
return False, (
|
|
571
|
+
f"Safety restriction: Cannot write to file "
|
|
572
|
+
f"outside of working directory {self.working_dir}"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Check dangerous commands
|
|
576
|
+
elif base_cmd in [
|
|
577
|
+
'sudo',
|
|
578
|
+
'su',
|
|
579
|
+
'chmod',
|
|
580
|
+
'chown',
|
|
581
|
+
'chgrp',
|
|
582
|
+
'passwd',
|
|
583
|
+
'mkfs',
|
|
584
|
+
'fdisk',
|
|
585
|
+
'dd',
|
|
586
|
+
'shutdown',
|
|
587
|
+
'reboot',
|
|
588
|
+
'halt',
|
|
589
|
+
'poweroff',
|
|
590
|
+
'init',
|
|
591
|
+
]:
|
|
592
|
+
return False, (
|
|
593
|
+
f"Safety restriction: Command '{base_cmd}' may affect system "
|
|
594
|
+
f"security and is prohibited"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Check network commands
|
|
598
|
+
elif base_cmd in ['ssh', 'telnet', 'ftp', 'sftp', 'nc', 'netcat']:
|
|
599
|
+
return False, (
|
|
600
|
+
f"Safety restriction: Network command '{base_cmd}' "
|
|
601
|
+
f"is prohibited"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Add copy functionality - copy from external to working directory
|
|
605
|
+
elif base_cmd == 'safecopy':
|
|
606
|
+
# Custom command: safecopy <source file> <target file>
|
|
607
|
+
if len(parts) != 3:
|
|
608
|
+
return False, "Usage: safecopy <source file> <target file>"
|
|
609
|
+
|
|
610
|
+
source = parts[1].strip('\'"')
|
|
611
|
+
target = parts[2].strip('\'"')
|
|
612
|
+
|
|
613
|
+
# Check if source file exists
|
|
614
|
+
if not os.path.exists(source):
|
|
615
|
+
return False, f"Source file does not exist: {source}"
|
|
616
|
+
|
|
617
|
+
# Ensure target is within working directory
|
|
618
|
+
if (
|
|
619
|
+
target.startswith('/')
|
|
620
|
+
or target.startswith('\\')
|
|
621
|
+
or ':' in target
|
|
622
|
+
):
|
|
623
|
+
# Absolute path
|
|
624
|
+
abs_target = os.path.abspath(target)
|
|
625
|
+
else:
|
|
626
|
+
# Relative path
|
|
627
|
+
abs_target = os.path.abspath(os.path.join(exec_dir, target))
|
|
628
|
+
|
|
629
|
+
if not self._is_path_within_working_dir(abs_target):
|
|
630
|
+
return False, (
|
|
631
|
+
f"Safety restriction: Target file must be within "
|
|
632
|
+
f"working directory {self.working_dir}"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Replace with safe copy command
|
|
636
|
+
if self.os_type == 'Windows':
|
|
637
|
+
return True, f"copy \"{source}\" \"{abs_target}\""
|
|
638
|
+
else:
|
|
639
|
+
return True, f"cp \"{source}\" \"{abs_target}\""
|
|
640
|
+
|
|
641
|
+
return True, command
|
|
642
|
+
|
|
643
|
+
def shell_exec(self, id: str, command: str) -> str:
|
|
644
|
+
r"""Execute commands. This can be used to execute various commands,
|
|
645
|
+
such as writing code, executing code, and running commands.
|
|
137
646
|
|
|
138
647
|
Args:
|
|
139
648
|
id (str): Unique identifier of the target shell session.
|
|
140
|
-
exec_dir (str): Working directory for command execution (must use
|
|
141
|
-
absolute path).
|
|
142
649
|
command (str): Shell command to execute.
|
|
143
650
|
|
|
144
651
|
Returns:
|
|
145
652
|
str: Output of the command execution or error message.
|
|
146
653
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
654
|
+
# Command execution must be within the working directory
|
|
655
|
+
error_msg = self._enforce_working_dir_for_execution(self.working_dir)
|
|
656
|
+
if error_msg:
|
|
657
|
+
return error_msg
|
|
149
658
|
|
|
150
|
-
if
|
|
151
|
-
|
|
659
|
+
if self.safe_mode:
|
|
660
|
+
is_safe, sanitized_command = self._sanitize_command(
|
|
661
|
+
command, self.working_dir
|
|
662
|
+
)
|
|
663
|
+
if not is_safe:
|
|
664
|
+
return f"Command rejected: {sanitized_command}"
|
|
665
|
+
command = sanitized_command
|
|
152
666
|
|
|
153
|
-
# If the session
|
|
667
|
+
# If the session does not exist, create a new session
|
|
154
668
|
if id not in self.shell_sessions:
|
|
155
669
|
self.shell_sessions[id] = {
|
|
156
670
|
"process": None,
|
|
@@ -159,48 +673,97 @@ class TerminalToolkit(BaseToolkit):
|
|
|
159
673
|
}
|
|
160
674
|
|
|
161
675
|
try:
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
676
|
+
# First, log the command to be executed
|
|
677
|
+
self._update_terminal_output(f"\n$ {command}\n")
|
|
678
|
+
|
|
679
|
+
if command.startswith('python') or command.startswith('pip'):
|
|
680
|
+
if self.cloned_env_path:
|
|
681
|
+
if self.os_type == 'Windows':
|
|
682
|
+
base_path = os.path.join(
|
|
683
|
+
self.cloned_env_path, "Scripts"
|
|
684
|
+
)
|
|
685
|
+
python_path = os.path.join(base_path, "python.exe")
|
|
686
|
+
pip_path = os.path.join(base_path, "pip.exe")
|
|
687
|
+
else:
|
|
688
|
+
base_path = os.path.join(self.cloned_env_path, "bin")
|
|
689
|
+
python_path = os.path.join(base_path, "python")
|
|
690
|
+
pip_path = os.path.join(base_path, "pip")
|
|
691
|
+
else:
|
|
692
|
+
python_path = self.python_executable
|
|
693
|
+
pip_path = f'"{python_path}" -m pip'
|
|
694
|
+
|
|
695
|
+
if command.startswith('python'):
|
|
696
|
+
command = command.replace('python', f'"{python_path}"', 1)
|
|
697
|
+
elif command.startswith('pip'):
|
|
698
|
+
command = command.replace('pip', pip_path, 1)
|
|
699
|
+
|
|
700
|
+
if self.is_macos:
|
|
701
|
+
# Type safe version - macOS uses subprocess.run
|
|
702
|
+
process = subprocess.run(
|
|
703
|
+
command,
|
|
704
|
+
shell=True,
|
|
705
|
+
cwd=self.working_dir,
|
|
706
|
+
capture_output=True,
|
|
707
|
+
text=True,
|
|
708
|
+
env=os.environ.copy(),
|
|
709
|
+
)
|
|
177
710
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
if process.stdout:
|
|
182
|
-
stdout = process.stdout.read().decode('utf-8')
|
|
711
|
+
# Process the output
|
|
712
|
+
output = process.stdout or ""
|
|
183
713
|
if process.stderr:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
714
|
+
output += f"\nStderr Output:\n{process.stderr}"
|
|
715
|
+
|
|
716
|
+
# Update session information and terminal
|
|
717
|
+
self.shell_sessions[id]["output"] = output
|
|
718
|
+
self._update_terminal_output(output + "\n")
|
|
719
|
+
|
|
720
|
+
return output
|
|
721
|
+
|
|
722
|
+
else:
|
|
723
|
+
# Non-macOS systems use the Popen method
|
|
724
|
+
proc = subprocess.Popen(
|
|
725
|
+
command,
|
|
726
|
+
shell=True,
|
|
727
|
+
cwd=self.working_dir,
|
|
728
|
+
stdout=subprocess.PIPE,
|
|
729
|
+
stderr=subprocess.PIPE,
|
|
730
|
+
stdin=subprocess.PIPE,
|
|
731
|
+
text=True,
|
|
732
|
+
bufsize=1,
|
|
733
|
+
universal_newlines=True,
|
|
734
|
+
env=os.environ.copy(),
|
|
735
|
+
)
|
|
188
736
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
737
|
+
# Store the process and mark it as running
|
|
738
|
+
self.shell_sessions[id]["process"] = proc
|
|
739
|
+
self.shell_sessions[id]["running"] = True
|
|
192
740
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
f"Command started in session '{id}'. Initial output: {output}"
|
|
196
|
-
)
|
|
741
|
+
# Get output
|
|
742
|
+
stdout, stderr = proc.communicate()
|
|
197
743
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
744
|
+
output = stdout or ""
|
|
745
|
+
if stderr:
|
|
746
|
+
output += f"\nStderr Output:\n{stderr}"
|
|
747
|
+
|
|
748
|
+
# Update session information and terminal
|
|
749
|
+
self.shell_sessions[id]["output"] = output
|
|
750
|
+
self._update_terminal_output(output + "\n")
|
|
751
|
+
|
|
752
|
+
return output
|
|
753
|
+
|
|
754
|
+
except Exception as e:
|
|
755
|
+
error_msg = f"Command execution error: {e!s}"
|
|
202
756
|
logger.error(error_msg)
|
|
203
|
-
|
|
757
|
+
self._update_terminal_output(f"\nError: {error_msg}\n")
|
|
758
|
+
|
|
759
|
+
# More detailed error information
|
|
760
|
+
import traceback
|
|
761
|
+
|
|
762
|
+
detailed_error = traceback.format_exc()
|
|
763
|
+
return (
|
|
764
|
+
f"Error: {error_msg}\n\n"
|
|
765
|
+
f"Detailed information: {detailed_error}"
|
|
766
|
+
)
|
|
204
767
|
|
|
205
768
|
def shell_view(self, id: str) -> str:
|
|
206
769
|
r"""View the content of a specified shell session.
|
|
@@ -215,50 +778,29 @@ class TerminalToolkit(BaseToolkit):
|
|
|
215
778
|
return f"Shell session not found: {id}"
|
|
216
779
|
|
|
217
780
|
session = self.shell_sessions[id]
|
|
218
|
-
process = session.get("process")
|
|
219
781
|
|
|
220
|
-
|
|
221
|
-
|
|
782
|
+
try:
|
|
783
|
+
# Check process status
|
|
784
|
+
if session["process"].poll() is not None:
|
|
785
|
+
session["running"] = False
|
|
222
786
|
|
|
223
|
-
|
|
224
|
-
|
|
787
|
+
# Collect all new output from agent queue
|
|
788
|
+
new_output = ""
|
|
225
789
|
try:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if stdout_data:
|
|
234
|
-
session["output"] += stdout_data
|
|
235
|
-
if stderr_data:
|
|
236
|
-
session["output"] += f"\nErrors:\n{stderr_data}"
|
|
237
|
-
except Exception as e:
|
|
238
|
-
logger.error(f"Error getting process output: {e}")
|
|
239
|
-
return f"Error: {e!s}"
|
|
790
|
+
while True:
|
|
791
|
+
output = self.agent_queue.get_nowait()
|
|
792
|
+
new_output += output
|
|
793
|
+
session["output"] += output
|
|
794
|
+
except queue.Empty:
|
|
795
|
+
pass
|
|
240
796
|
|
|
241
|
-
|
|
242
|
-
if process.poll() is not None and session["running"]:
|
|
243
|
-
try:
|
|
244
|
-
# Get remaining output if any
|
|
245
|
-
stdout_data, stderr_data = "", ""
|
|
246
|
-
if process.stdout and process.stdout.readable():
|
|
247
|
-
stdout_data = process.stdout.read().decode('utf-8')
|
|
248
|
-
if process.stderr and process.stderr.readable():
|
|
249
|
-
stderr_data = process.stderr.read().decode('utf-8')
|
|
250
|
-
|
|
251
|
-
if stdout_data:
|
|
252
|
-
session["output"] += stdout_data
|
|
253
|
-
if stderr_data:
|
|
254
|
-
session["output"] += f"\nErrors:\n{stderr_data}"
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error(f"Error getting final process output: {e}")
|
|
257
|
-
return f"Error: {e!s}"
|
|
258
|
-
finally:
|
|
259
|
-
session["running"] = False
|
|
797
|
+
return new_output or session["output"]
|
|
260
798
|
|
|
261
|
-
|
|
799
|
+
except Exception as e:
|
|
800
|
+
error_msg = f"Error reading terminal output: {e}"
|
|
801
|
+
self._update_terminal_output(f"\nError: {error_msg}\n")
|
|
802
|
+
logger.error(error_msg)
|
|
803
|
+
return f"Error: {e!s}"
|
|
262
804
|
|
|
263
805
|
def shell_wait(self, id: str, seconds: Optional[int] = None) -> str:
|
|
264
806
|
r"""Wait for the running process in a specified shell session to
|
|
@@ -281,33 +823,40 @@ class TerminalToolkit(BaseToolkit):
|
|
|
281
823
|
if process is None:
|
|
282
824
|
return f"No active process in session '{id}'"
|
|
283
825
|
|
|
284
|
-
if not session["running"]:
|
|
826
|
+
if not session["running"] or process.poll() is not None:
|
|
285
827
|
return f"Process in session '{id}' is not running"
|
|
286
828
|
|
|
287
829
|
try:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
830
|
+
if hasattr(process, 'communicate'):
|
|
831
|
+
# Use communicate with timeout
|
|
832
|
+
stdout, stderr = process.communicate(timeout=seconds)
|
|
833
|
+
|
|
834
|
+
if stdout:
|
|
835
|
+
stdout_str = (
|
|
836
|
+
stdout.decode('utf-8')
|
|
837
|
+
if isinstance(stdout, bytes)
|
|
838
|
+
else stdout
|
|
839
|
+
)
|
|
840
|
+
session["output"] += stdout_str
|
|
841
|
+
if stderr:
|
|
842
|
+
stderr_str = (
|
|
843
|
+
stderr.decode('utf-8')
|
|
844
|
+
if isinstance(stderr, bytes)
|
|
845
|
+
else stderr
|
|
846
|
+
)
|
|
847
|
+
if stderr_str:
|
|
848
|
+
session["output"] += f"\nStderr Output:\n{stderr_str}"
|
|
849
|
+
|
|
850
|
+
session["running"] = False
|
|
851
|
+
return (
|
|
852
|
+
f"Process completed in session '{id}'. "
|
|
853
|
+
f"Output: {session['output']}"
|
|
296
854
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if isinstance(stderr, bytes)
|
|
302
|
-
else stderr
|
|
855
|
+
else:
|
|
856
|
+
return (
|
|
857
|
+
f"Process already completed in session '{id}'. "
|
|
858
|
+
f"Output: {session['output']}"
|
|
303
859
|
)
|
|
304
|
-
session["output"] += f"\nErrors:\n{stderr_str}"
|
|
305
|
-
|
|
306
|
-
session["running"] = False
|
|
307
|
-
return (
|
|
308
|
-
f"Process completed in session '{id}'. "
|
|
309
|
-
f"Output: {session['output']}"
|
|
310
|
-
)
|
|
311
860
|
|
|
312
861
|
except subprocess.TimeoutExpired:
|
|
313
862
|
return (
|
|
@@ -404,6 +953,71 @@ class TerminalToolkit(BaseToolkit):
|
|
|
404
953
|
logger.error(f"Error killing process: {e}")
|
|
405
954
|
return f"Error killing process: {e!s}"
|
|
406
955
|
|
|
956
|
+
def __del__(self):
|
|
957
|
+
r"""Clean up resources when the object is being destroyed.
|
|
958
|
+
Terminates all running processes and closes any open file handles.
|
|
959
|
+
"""
|
|
960
|
+
# Log that cleanup is starting
|
|
961
|
+
logger.info("TerminalToolkit cleanup initiated")
|
|
962
|
+
|
|
963
|
+
# Clean up all processes in shell sessions
|
|
964
|
+
for session_id, session in self.shell_sessions.items():
|
|
965
|
+
process = session.get("process")
|
|
966
|
+
if process is not None and session.get("running", False):
|
|
967
|
+
try:
|
|
968
|
+
logger.info(
|
|
969
|
+
f"Terminating process in session '{session_id}'"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# Close process input/output streams if open
|
|
973
|
+
if (
|
|
974
|
+
hasattr(process, 'stdin')
|
|
975
|
+
and process.stdin
|
|
976
|
+
and not process.stdin.closed
|
|
977
|
+
):
|
|
978
|
+
process.stdin.close()
|
|
979
|
+
|
|
980
|
+
# Terminate the process
|
|
981
|
+
process.terminate()
|
|
982
|
+
try:
|
|
983
|
+
# Give the process a short time to terminate gracefully
|
|
984
|
+
process.wait(timeout=3)
|
|
985
|
+
except subprocess.TimeoutExpired:
|
|
986
|
+
# Force kill if the process doesn't terminate
|
|
987
|
+
# gracefully
|
|
988
|
+
logger.warning(
|
|
989
|
+
f"Process in session '{session_id}' did not "
|
|
990
|
+
f"terminate gracefully, forcing kill"
|
|
991
|
+
)
|
|
992
|
+
process.kill()
|
|
993
|
+
|
|
994
|
+
# Mark the session as not running
|
|
995
|
+
session["running"] = False
|
|
996
|
+
|
|
997
|
+
except Exception as e:
|
|
998
|
+
logger.error(
|
|
999
|
+
f"Error cleaning up process in session "
|
|
1000
|
+
f"'{session_id}': {e}"
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
# Close file output if it exists
|
|
1004
|
+
if hasattr(self, 'log_file') and self.is_macos:
|
|
1005
|
+
try:
|
|
1006
|
+
logger.info(f"Final terminal log saved to: {self.log_file}")
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
logger.error(f"Error logging file information: {e}")
|
|
1009
|
+
|
|
1010
|
+
# Clean up GUI resources if they exist
|
|
1011
|
+
if hasattr(self, 'root') and self.root:
|
|
1012
|
+
try:
|
|
1013
|
+
logger.info("Closing terminal GUI")
|
|
1014
|
+
self.root.quit()
|
|
1015
|
+
self.root.destroy()
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
logger.error(f"Error closing terminal GUI: {e}")
|
|
1018
|
+
|
|
1019
|
+
logger.info("TerminalToolkit cleanup completed")
|
|
1020
|
+
|
|
407
1021
|
def get_tools(self) -> List[FunctionTool]:
|
|
408
1022
|
r"""Returns a list of FunctionTool objects representing the functions
|
|
409
1023
|
in the toolkit.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
camel/__init__.py,sha256=
|
|
1
|
+
camel/__init__.py,sha256=OFoHvtZQPjUxj6ga19ljZ11Bxul4-gtgGIZnBFLOBvU,912
|
|
2
2
|
camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
|
|
3
3
|
camel/human.py,sha256=9X09UmxI2JqQnhrFfnZ3B9EzFmVfdSWQcjLWTIXKXe0,4962
|
|
4
4
|
camel/logger.py,sha256=rZVeOVYuQ9RYJ5Tqyv0usqy0g4zaVEq4qSfZ9nd2640,5755
|
|
@@ -7,7 +7,7 @@ camel/agents/__init__.py,sha256=OEWDfxima22hjSZWgIu6sn9O_QKBr6cpS8b1Jywou0A,1596
|
|
|
7
7
|
camel/agents/_types.py,sha256=ryPRmEXnpNtbFT23GoAcwK-zxWWsIOqYu64mxMx_PhI,1430
|
|
8
8
|
camel/agents/_utils.py,sha256=AR7Qqgbkmn4X2edYUQf1rdksGUyV5hm3iK1z-Dn0Mcg,6266
|
|
9
9
|
camel/agents/base.py,sha256=c4bJYL3G3Z41SaFdMPMn8ZjLdFiFaVOFO6EQIfuCVR8,1124
|
|
10
|
-
camel/agents/chat_agent.py,sha256=
|
|
10
|
+
camel/agents/chat_agent.py,sha256=74Wd9ESQqepLrGcJIo0lU-5QAHCHIB7zS0-NiqiC7HA,52035
|
|
11
11
|
camel/agents/critic_agent.py,sha256=qFVlHlQo0CVgmPWfWYLT8_oP_KyzCLFsQw_nN_vu5Bs,7487
|
|
12
12
|
camel/agents/deductive_reasoner_agent.py,sha256=6BZGaq1hR6hKJuQtOfoYQnk_AkZpw_Mr7mUy2MspQgs,13540
|
|
13
13
|
camel/agents/embodied_agent.py,sha256=XBxBu5ZMmSJ4B2U3Z7SMwvLlgp6yNpaBe8HNQmY9CZA,7536
|
|
@@ -106,7 +106,7 @@ camel/embeddings/vlm_embedding.py,sha256=HZFdcz1YzkFPzMj45_jaCVmDQJyccoXN561aLWl
|
|
|
106
106
|
camel/environments/__init__.py,sha256=nWFEYK-QcRX0WskLVsXn4iP_pK2liL-iHKG7DSO0zTU,969
|
|
107
107
|
camel/environments/models.py,sha256=jVcCyU7xObKoWPnkshmPqyyKi3AOiMVVtUZA-tWEYUU,4194
|
|
108
108
|
camel/environments/multi_step.py,sha256=rEEMMHip5ZVEWpNj7miws5wALlujtUbFZkWDsy7ofHM,8360
|
|
109
|
-
camel/environments/single_step.py,sha256=
|
|
109
|
+
camel/environments/single_step.py,sha256=ELZ5GViw9eFCjQMytPMaLWHbAZZAqUCmNsk9h6qbggU,19713
|
|
110
110
|
camel/extractors/__init__.py,sha256=lgtDl8zWvN826fJVKqRv05w556YZ-EdrHwdzKphywgA,1097
|
|
111
111
|
camel/extractors/base.py,sha256=3jvuZpq27nlADDCX3GfubOpeb_zt-E9rzxF3x4lYm8s,10404
|
|
112
112
|
camel/extractors/python_strategies.py,sha256=k8q4BIAhPZnCSN2LqPaZVrhF56y3Y4cZ6ddn79jcIXE,7825
|
|
@@ -180,7 +180,7 @@ camel/models/sglang_model.py,sha256=kdF-qShOH4j5lkuC2JEzUByuSoKtCFHdrIpAdbpR2Gg,
|
|
|
180
180
|
camel/models/siliconflow_model.py,sha256=c40e0SQjHUNjr1ttJTTRTylRiNsPK_idP7Pa2iZr36g,6041
|
|
181
181
|
camel/models/stub_model.py,sha256=JvjeEkXS7RMcR_UA_64a3T6S0QALUhOaMQs-aI7Etug,5955
|
|
182
182
|
camel/models/togetherai_model.py,sha256=nV6ZqOrwEK6oNU1gTJlPfJDQbd9Mcl4GWkbqYnGPCQ0,7049
|
|
183
|
-
camel/models/vllm_model.py,sha256=
|
|
183
|
+
camel/models/vllm_model.py,sha256=lbcflI19mnvMt62hvpdDpfIJqYo0fTsd-pEZC_H-akA,8826
|
|
184
184
|
camel/models/volcano_model.py,sha256=inYDiKOfGvq8o3XW4KVQIrXiZOhXQfB4HfCHGCWHPKs,3792
|
|
185
185
|
camel/models/yi_model.py,sha256=sg7qOzvWZlGeKmlvA4kvZSWwMxTBo0-qgvEVjBalXcE,6572
|
|
186
186
|
camel/models/zhipuai_model.py,sha256=JtMOTDsJIteZBPdIAwkeyUoAoh3O0dseaqjikiwjIfM,6909
|
|
@@ -312,7 +312,7 @@ camel/toolkits/semantic_scholar_toolkit.py,sha256=Rh7eA_YPxV5pvPIzhjjvpr3vtlaCni
|
|
|
312
312
|
camel/toolkits/slack_toolkit.py,sha256=F1Xn2_Jmnv-1SdBnCNg3MI3RGwjZ7ZDWiNZjFJJS6x8,10791
|
|
313
313
|
camel/toolkits/stripe_toolkit.py,sha256=07swo5znGTnorafC1uYLKB4NRcJIOPOx19J7tkpLYWk,10102
|
|
314
314
|
camel/toolkits/sympy_toolkit.py,sha256=dkzGp7C7Oy-qP1rVziEk_ZOPRb37d5LoI7JKCLiTEo4,33758
|
|
315
|
-
camel/toolkits/terminal_toolkit.py,sha256=
|
|
315
|
+
camel/toolkits/terminal_toolkit.py,sha256=gupuTvNkwnFzcFwDB_irSJ9-dXRr8yEAsYq5ChEkkHg,37230
|
|
316
316
|
camel/toolkits/thinking_toolkit.py,sha256=NyA6rDFG-WbCNt7NFODBTpqOIDtP6he6GhnZpPlA2To,8001
|
|
317
317
|
camel/toolkits/twitter_toolkit.py,sha256=j14Hxpt4XTEOF6dWpGm6Ot_vkYT7WOiLrUa1d2JT05U,15850
|
|
318
318
|
camel/toolkits/video_analysis_toolkit.py,sha256=EqZnut9p0W5bgF1YpdGoaRJFDLHM2Ls8i9ApTQz44tA,15088
|
|
@@ -368,7 +368,7 @@ camel/verifiers/base.py,sha256=SQGZPP6p08q4Qmpr1vD-eb0UxBwkl1hpZSm19yV2wWo,14866
|
|
|
368
368
|
camel/verifiers/math_verifier.py,sha256=tA1D4S0sm8nsWISevxSN0hvSVtIUpqmJhzqfbuMo0y4,6875
|
|
369
369
|
camel/verifiers/models.py,sha256=GdxYPr7UxNrR1577yW4kyroRcLGfd-H1GXgv8potDWU,2471
|
|
370
370
|
camel/verifiers/python_verifier.py,sha256=o1VVINq4YQqNPz8h1FiZ7v2dMBN3wlH6AsRWPqN0iGQ,18037
|
|
371
|
-
camel_ai-0.2.
|
|
372
|
-
camel_ai-0.2.
|
|
373
|
-
camel_ai-0.2.
|
|
374
|
-
camel_ai-0.2.
|
|
371
|
+
camel_ai-0.2.42.dist-info/METADATA,sha256=Yi_gfzN4krlk4P1Ek1XKLMuXldj0Xwu5TdddMd-pQqc,42033
|
|
372
|
+
camel_ai-0.2.42.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
373
|
+
camel_ai-0.2.42.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
|
|
374
|
+
camel_ai-0.2.42.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|