camel-ai 0.2.23__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/configs/anthropic_config.py +45 -11
- camel/models/anthropic_model.py +5 -1
- camel/societies/role_playing.py +119 -0
- camel/toolkits/__init__.py +6 -0
- camel/toolkits/file_write_toolkit.py +371 -0
- camel/toolkits/mcp_toolkit.py +251 -0
- camel/toolkits/terminal_toolkit.py +421 -0
- camel/toolkits/web_toolkit.py +4 -5
- camel/types/enums.py +3 -0
- {camel_ai-0.2.23.dist-info → camel_ai-0.2.24.dist-info}/METADATA +12 -3
- {camel_ai-0.2.23.dist-info → camel_ai-0.2.24.dist-info}/RECORD +14 -11
- {camel_ai-0.2.23.dist-info → camel_ai-0.2.24.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.23.dist-info → camel_ai-0.2.24.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
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 inspect
|
|
15
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
16
|
+
from typing import (
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Any,
|
|
19
|
+
Callable,
|
|
20
|
+
Dict,
|
|
21
|
+
List,
|
|
22
|
+
Optional,
|
|
23
|
+
Set,
|
|
24
|
+
Union,
|
|
25
|
+
)
|
|
26
|
+
from urllib.parse import urlparse
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from mcp import ListToolsResult, Tool
|
|
30
|
+
|
|
31
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MCPToolkit(BaseToolkit):
|
|
35
|
+
r"""MCPToolkit provides an abstraction layer to interact with external
|
|
36
|
+
tools using the Model Context Protocol (MCP). It supports two modes of
|
|
37
|
+
connection:
|
|
38
|
+
|
|
39
|
+
1. stdio mode: Connects via standard input/output streams for local
|
|
40
|
+
command-line interactions.
|
|
41
|
+
|
|
42
|
+
2. SSE mode (HTTP Server-Sent Events): Connects via HTTP for persistent,
|
|
43
|
+
event-based interactions.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
command_or_url (str): URL for SSE mode or command executable for stdio
|
|
47
|
+
mode. (default: :obj:`'None'`)
|
|
48
|
+
args (List[str]): List of command-line arguments if stdio mode is used.
|
|
49
|
+
(default: :obj:`'None'`)
|
|
50
|
+
env (Dict[str, str]): Environment variables for the stdio mode command.
|
|
51
|
+
(default: :obj:`'None'`)
|
|
52
|
+
timeout (Optional[float]): Connection timeout. (default: :obj:`'None'`)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
command_or_url: str,
|
|
58
|
+
args: Optional[List[str]] = None,
|
|
59
|
+
env: Optional[Dict[str, str]] = None,
|
|
60
|
+
timeout: Optional[float] = None,
|
|
61
|
+
):
|
|
62
|
+
from mcp import Tool
|
|
63
|
+
from mcp.client.session import ClientSession
|
|
64
|
+
|
|
65
|
+
super().__init__(timeout=timeout)
|
|
66
|
+
|
|
67
|
+
self.command_or_url = command_or_url
|
|
68
|
+
self.args = args or []
|
|
69
|
+
self.env = env or {}
|
|
70
|
+
|
|
71
|
+
self._mcp_tools: List[Tool] = []
|
|
72
|
+
self._session: Optional['ClientSession'] = None
|
|
73
|
+
self._exit_stack = AsyncExitStack()
|
|
74
|
+
self._is_connected = False
|
|
75
|
+
|
|
76
|
+
@asynccontextmanager
|
|
77
|
+
async def connection(self):
|
|
78
|
+
r"""Async context manager for establishing and managing the connection
|
|
79
|
+
with the MCP server. Automatically selects SSE or stdio mode based
|
|
80
|
+
on the provided `command_or_url`.
|
|
81
|
+
|
|
82
|
+
Yields:
|
|
83
|
+
MCPToolkit: Instance with active connection ready for tool
|
|
84
|
+
interaction.
|
|
85
|
+
"""
|
|
86
|
+
from mcp.client.session import ClientSession
|
|
87
|
+
from mcp.client.sse import sse_client
|
|
88
|
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
if urlparse(self.command_or_url).scheme in ("http", "https"):
|
|
92
|
+
(
|
|
93
|
+
read_stream,
|
|
94
|
+
write_stream,
|
|
95
|
+
) = await self._exit_stack.enter_async_context(
|
|
96
|
+
sse_client(self.command_or_url)
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
server_parameters = StdioServerParameters(
|
|
100
|
+
command=self.command_or_url, args=self.args, env=self.env
|
|
101
|
+
)
|
|
102
|
+
(
|
|
103
|
+
read_stream,
|
|
104
|
+
write_stream,
|
|
105
|
+
) = await self._exit_stack.enter_async_context(
|
|
106
|
+
stdio_client(server_parameters)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self._session = await self._exit_stack.enter_async_context(
|
|
110
|
+
ClientSession(read_stream, write_stream)
|
|
111
|
+
)
|
|
112
|
+
await self._session.initialize()
|
|
113
|
+
list_tools_result = await self.list_mcp_tools()
|
|
114
|
+
self._mcp_tools = list_tools_result.tools
|
|
115
|
+
self._is_connected = True
|
|
116
|
+
yield self
|
|
117
|
+
|
|
118
|
+
finally:
|
|
119
|
+
self._is_connected = False
|
|
120
|
+
await self._exit_stack.aclose()
|
|
121
|
+
self._session = None
|
|
122
|
+
|
|
123
|
+
async def list_mcp_tools(self) -> Union[str, "ListToolsResult"]:
|
|
124
|
+
r"""Retrieves the list of available tools from the connected MCP
|
|
125
|
+
server.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
ListToolsResult: Result containing available MCP tools.
|
|
129
|
+
"""
|
|
130
|
+
if not self._session:
|
|
131
|
+
return "MCP Client is not connected. Call `connection()` first."
|
|
132
|
+
try:
|
|
133
|
+
return await self._session.list_tools()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return f"Failed to list MCP tools: {e!s}"
|
|
136
|
+
|
|
137
|
+
def generate_function_from_mcp_tool(self, mcp_tool: "Tool") -> Callable:
|
|
138
|
+
r"""Dynamically generates a Python callable function corresponding to
|
|
139
|
+
a given MCP tool.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
mcp_tool (Tool): The MCP tool definition received from the MCP
|
|
143
|
+
server.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Callable: A dynamically created async Python function that wraps
|
|
147
|
+
the MCP tool.
|
|
148
|
+
"""
|
|
149
|
+
func_name = mcp_tool.name
|
|
150
|
+
func_desc = mcp_tool.description or "No description provided."
|
|
151
|
+
parameters_schema = mcp_tool.inputSchema.get("properties", {})
|
|
152
|
+
required_params = mcp_tool.inputSchema.get("required", [])
|
|
153
|
+
|
|
154
|
+
type_map = {
|
|
155
|
+
"string": str,
|
|
156
|
+
"integer": int,
|
|
157
|
+
"number": float,
|
|
158
|
+
"boolean": bool,
|
|
159
|
+
"array": list,
|
|
160
|
+
"object": dict,
|
|
161
|
+
}
|
|
162
|
+
annotations = {} # used to type hints
|
|
163
|
+
defaults: Dict[str, Any] = {} # store default values
|
|
164
|
+
|
|
165
|
+
func_params = []
|
|
166
|
+
for param_name, param_schema in parameters_schema.items():
|
|
167
|
+
param_type = param_schema.get("type", "Any")
|
|
168
|
+
param_type = type_map.get(param_type, Any)
|
|
169
|
+
|
|
170
|
+
annotations[param_name] = param_type
|
|
171
|
+
if param_name not in required_params:
|
|
172
|
+
defaults[param_name] = None
|
|
173
|
+
|
|
174
|
+
func_params.append(param_name)
|
|
175
|
+
|
|
176
|
+
async def dynamic_function(**kwargs):
|
|
177
|
+
r"""Auto-generated function for MCP Tool interaction.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
kwargs: Keyword arguments corresponding to MCP tool parameters.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: The textual result returned by the MCP tool.
|
|
184
|
+
"""
|
|
185
|
+
from mcp.types import CallToolResult
|
|
186
|
+
|
|
187
|
+
missing_params: Set[str] = set(required_params) - set(
|
|
188
|
+
kwargs.keys()
|
|
189
|
+
)
|
|
190
|
+
if missing_params:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Missing required parameters: {missing_params}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
result: CallToolResult = await self._session.call_tool(
|
|
196
|
+
func_name, kwargs
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not result.content:
|
|
200
|
+
return "No data available for this request."
|
|
201
|
+
|
|
202
|
+
# Handle different content types
|
|
203
|
+
content = result.content[0]
|
|
204
|
+
if content.type == "text":
|
|
205
|
+
return content.text
|
|
206
|
+
elif content.type == "image":
|
|
207
|
+
# Return image URL or data URI if available
|
|
208
|
+
if hasattr(content, "url") and content.url:
|
|
209
|
+
return f"Image available at: {content.url}"
|
|
210
|
+
return "Image content received (data URI not shown)"
|
|
211
|
+
elif content.type == "embedded_resource":
|
|
212
|
+
# Return resource information if available
|
|
213
|
+
if hasattr(content, "name") and content.name:
|
|
214
|
+
return f"Embedded resource: {content.name}"
|
|
215
|
+
return "Embedded resource received"
|
|
216
|
+
else:
|
|
217
|
+
msg = f"Received content of type '{content.type}'"
|
|
218
|
+
return f"{msg} which is not fully supported yet."
|
|
219
|
+
|
|
220
|
+
dynamic_function.__name__ = func_name
|
|
221
|
+
dynamic_function.__doc__ = func_desc
|
|
222
|
+
dynamic_function.__annotations__ = annotations
|
|
223
|
+
|
|
224
|
+
sig = inspect.Signature(
|
|
225
|
+
parameters=[
|
|
226
|
+
inspect.Parameter(
|
|
227
|
+
name=param,
|
|
228
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
229
|
+
default=defaults.get(param, inspect.Parameter.empty),
|
|
230
|
+
annotation=annotations[param],
|
|
231
|
+
)
|
|
232
|
+
for param in func_params
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
dynamic_function.__signature__ = sig # type: ignore[attr-defined]
|
|
236
|
+
|
|
237
|
+
return dynamic_function
|
|
238
|
+
|
|
239
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
240
|
+
r"""Returns a list of FunctionTool objects representing the
|
|
241
|
+
functions in the toolkit. Each function is dynamically generated
|
|
242
|
+
based on the MCP tool definitions received from the server.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List[FunctionTool]: A list of FunctionTool objects
|
|
246
|
+
representing the functions in the toolkit.
|
|
247
|
+
"""
|
|
248
|
+
return [
|
|
249
|
+
FunctionTool(self.generate_function_from_mcp_tool(mcp_tool))
|
|
250
|
+
for mcp_tool in self._mcp_tools
|
|
251
|
+
]
|
|
@@ -0,0 +1,421 @@
|
|
|
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
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
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 TerminalToolkit(BaseToolkit):
|
|
27
|
+
r"""A toolkit for terminal operations across multiple operating systems.
|
|
28
|
+
|
|
29
|
+
This toolkit provides a set of functions for terminal operations such as
|
|
30
|
+
searching for files by name or content, executing shell commands, and
|
|
31
|
+
managing terminal sessions.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
timeout (Optional[float]): The timeout for terminal operations.
|
|
35
|
+
shell_sessions (Optional[Dict[str, Any]]): A dictionary to store
|
|
36
|
+
shell session information. If None, an empty dictionary will be
|
|
37
|
+
used.
|
|
38
|
+
|
|
39
|
+
Note:
|
|
40
|
+
Most functions are compatible with Unix-based systems (macOS, Linux).
|
|
41
|
+
For Windows compatibility, additional implementation details are
|
|
42
|
+
needed.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
timeout: Optional[float] = None,
|
|
48
|
+
shell_sessions: Optional[Dict[str, Any]] = None,
|
|
49
|
+
):
|
|
50
|
+
import platform
|
|
51
|
+
|
|
52
|
+
super().__init__(timeout=timeout)
|
|
53
|
+
self.shell_sessions = shell_sessions or {}
|
|
54
|
+
self.os_type = (
|
|
55
|
+
platform.system()
|
|
56
|
+
) # 'Windows', 'Darwin' (macOS), 'Linux'
|
|
57
|
+
|
|
58
|
+
def file_find_in_content(
|
|
59
|
+
self, file: str, regex: str, sudo: bool = False
|
|
60
|
+
) -> str:
|
|
61
|
+
r"""Search for matching text within file content.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file (str): Absolute path of the file to search within.
|
|
65
|
+
regex (str): Regular expression pattern to match.
|
|
66
|
+
sudo (bool, optional): Whether to use sudo privileges. Defaults to
|
|
67
|
+
False. Note: Using sudo requires the process to have
|
|
68
|
+
appropriate permissions.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: Matching content found in the file.
|
|
72
|
+
"""
|
|
73
|
+
if not os.path.exists(file):
|
|
74
|
+
return f"File not found: {file}"
|
|
75
|
+
|
|
76
|
+
if not os.path.isfile(file):
|
|
77
|
+
return f"The path provided is not a file: {file}"
|
|
78
|
+
|
|
79
|
+
command = []
|
|
80
|
+
if sudo:
|
|
81
|
+
command.extend(["sudo"])
|
|
82
|
+
|
|
83
|
+
if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
|
|
84
|
+
command.extend(["grep", "-E", regex, file])
|
|
85
|
+
else: # Windows
|
|
86
|
+
# For Windows, we could use PowerShell or findstr
|
|
87
|
+
command.extend(["findstr", "/R", regex, file])
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
command, check=False, capture_output=True, text=True
|
|
92
|
+
)
|
|
93
|
+
return result.stdout.strip()
|
|
94
|
+
except subprocess.SubprocessError as e:
|
|
95
|
+
logger.error(f"Error searching in file content: {e}")
|
|
96
|
+
return f"Error: {e!s}"
|
|
97
|
+
|
|
98
|
+
def file_find_by_name(self, path: str, glob: str) -> str:
|
|
99
|
+
r"""Find files by name pattern in specified directory.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
path (str): Absolute path of directory to search.
|
|
103
|
+
glob (str): Filename pattern using glob syntax wildcards.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: List of files matching the pattern.
|
|
107
|
+
"""
|
|
108
|
+
if not os.path.exists(path):
|
|
109
|
+
return f"Directory not found: {path}"
|
|
110
|
+
|
|
111
|
+
if not os.path.isdir(path):
|
|
112
|
+
return f"The path provided is not a directory: {path}"
|
|
113
|
+
|
|
114
|
+
command = []
|
|
115
|
+
if self.os_type in ['Darwin', 'Linux']: # macOS or Linux
|
|
116
|
+
command.extend(["find", path, "-name", glob])
|
|
117
|
+
else: # Windows
|
|
118
|
+
# For Windows, we use dir command with /s for recursive search
|
|
119
|
+
# and /b for bare format
|
|
120
|
+
|
|
121
|
+
pattern = glob
|
|
122
|
+
command.extend(["dir", "/s", "/b", os.path.join(path, pattern)])
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
command, check=False, capture_output=True, text=True
|
|
127
|
+
)
|
|
128
|
+
return result.stdout.strip()
|
|
129
|
+
except subprocess.SubprocessError as e:
|
|
130
|
+
logger.error(f"Error finding files by name: {e}")
|
|
131
|
+
return f"Error: {e!s}"
|
|
132
|
+
|
|
133
|
+
def shell_exec(self, id: str, exec_dir: str, command: str) -> str:
|
|
134
|
+
r"""Execute commands in a specified shell session.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
id (str): Unique identifier of the target shell session.
|
|
138
|
+
exec_dir (str): Working directory for command execution (must use
|
|
139
|
+
absolute path).
|
|
140
|
+
command (str): Shell command to execute.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
str: Output of the command execution or error message.
|
|
144
|
+
"""
|
|
145
|
+
if not os.path.isabs(exec_dir):
|
|
146
|
+
return f"exec_dir must be an absolute path: {exec_dir}"
|
|
147
|
+
|
|
148
|
+
if not os.path.exists(exec_dir):
|
|
149
|
+
return f"Directory not found: {exec_dir}"
|
|
150
|
+
|
|
151
|
+
# If the session doesn't exist, create a new one
|
|
152
|
+
if id not in self.shell_sessions:
|
|
153
|
+
self.shell_sessions[id] = {
|
|
154
|
+
"process": None,
|
|
155
|
+
"output": "",
|
|
156
|
+
"running": False,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Execute the command in the specified directory
|
|
161
|
+
process = subprocess.Popen(
|
|
162
|
+
command,
|
|
163
|
+
shell=True,
|
|
164
|
+
cwd=exec_dir,
|
|
165
|
+
stdout=subprocess.PIPE,
|
|
166
|
+
stderr=subprocess.PIPE,
|
|
167
|
+
stdin=subprocess.PIPE,
|
|
168
|
+
text=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Store the process and mark as running
|
|
172
|
+
self.shell_sessions[id]["process"] = process
|
|
173
|
+
self.shell_sessions[id]["running"] = True
|
|
174
|
+
self.shell_sessions[id]["output"] = ""
|
|
175
|
+
|
|
176
|
+
# Get initial output (non-blocking)
|
|
177
|
+
stdout, stderr = "", ""
|
|
178
|
+
try:
|
|
179
|
+
if process.stdout:
|
|
180
|
+
stdout = process.stdout.read().decode('utf-8')
|
|
181
|
+
if process.stderr:
|
|
182
|
+
stderr = process.stderr.read().decode('utf-8')
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Error reading initial output: {e}")
|
|
185
|
+
return f"Error: {e!s}"
|
|
186
|
+
|
|
187
|
+
output = stdout
|
|
188
|
+
if stderr:
|
|
189
|
+
output += f"\nErrors:\n{stderr}"
|
|
190
|
+
|
|
191
|
+
self.shell_sessions[id]["output"] = output
|
|
192
|
+
return (
|
|
193
|
+
f"Command started in session '{id}'. Initial output: {output}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
except subprocess.SubprocessError as e:
|
|
197
|
+
self.shell_sessions[id]["running"] = False
|
|
198
|
+
error_msg = f"Error executing command: {e}"
|
|
199
|
+
self.shell_sessions[id]["output"] = error_msg
|
|
200
|
+
logger.error(error_msg)
|
|
201
|
+
return error_msg
|
|
202
|
+
|
|
203
|
+
def shell_view(self, id: str) -> str:
|
|
204
|
+
r"""View the content of a specified shell session.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
id (str): Unique identifier of the target shell session.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str: Current output content of the shell session.
|
|
211
|
+
"""
|
|
212
|
+
if id not in self.shell_sessions:
|
|
213
|
+
return f"Shell session not found: {id}"
|
|
214
|
+
|
|
215
|
+
session = self.shell_sessions[id]
|
|
216
|
+
process = session.get("process")
|
|
217
|
+
|
|
218
|
+
if process is None:
|
|
219
|
+
return f"No active process in session '{id}'"
|
|
220
|
+
|
|
221
|
+
# Try to get any new output
|
|
222
|
+
if session["running"] and process.poll() is None:
|
|
223
|
+
try:
|
|
224
|
+
# Non-blocking read from stdout/stderr
|
|
225
|
+
stdout_data, stderr_data = "", ""
|
|
226
|
+
if process.stdout and process.stdout.readable():
|
|
227
|
+
stdout_data = process.stdout.read1().decode('utf-8')
|
|
228
|
+
if process.stderr and process.stderr.readable():
|
|
229
|
+
stderr_data = process.stderr.read1().decode('utf-8')
|
|
230
|
+
|
|
231
|
+
if stdout_data:
|
|
232
|
+
session["output"] += stdout_data
|
|
233
|
+
if stderr_data:
|
|
234
|
+
session["output"] += f"\nErrors:\n{stderr_data}"
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Error getting process output: {e}")
|
|
237
|
+
return f"Error: {e!s}"
|
|
238
|
+
|
|
239
|
+
# Check if the process has completed
|
|
240
|
+
if process.poll() is not None and session["running"]:
|
|
241
|
+
try:
|
|
242
|
+
# Get remaining output if any
|
|
243
|
+
stdout_data, stderr_data = "", ""
|
|
244
|
+
if process.stdout and process.stdout.readable():
|
|
245
|
+
stdout_data = process.stdout.read().decode('utf-8')
|
|
246
|
+
if process.stderr and process.stderr.readable():
|
|
247
|
+
stderr_data = process.stderr.read().decode('utf-8')
|
|
248
|
+
|
|
249
|
+
if stdout_data:
|
|
250
|
+
session["output"] += stdout_data
|
|
251
|
+
if stderr_data:
|
|
252
|
+
session["output"] += f"\nErrors:\n{stderr_data}"
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Error getting final process output: {e}")
|
|
255
|
+
return f"Error: {e!s}"
|
|
256
|
+
finally:
|
|
257
|
+
session["running"] = False
|
|
258
|
+
|
|
259
|
+
return session["output"]
|
|
260
|
+
|
|
261
|
+
def shell_wait(self, id: str, seconds: Optional[int] = None) -> str:
|
|
262
|
+
r"""Wait for the running process in a specified shell session to
|
|
263
|
+
return.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
id (str): Unique identifier of the target shell session.
|
|
267
|
+
seconds (Optional[int], optional): Wait duration in seconds.
|
|
268
|
+
If None, wait indefinitely. Defaults to None.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
str: Final output content after waiting.
|
|
272
|
+
"""
|
|
273
|
+
if id not in self.shell_sessions:
|
|
274
|
+
return f"Shell session not found: {id}"
|
|
275
|
+
|
|
276
|
+
session = self.shell_sessions[id]
|
|
277
|
+
process = session.get("process")
|
|
278
|
+
|
|
279
|
+
if process is None:
|
|
280
|
+
return f"No active process in session '{id}'"
|
|
281
|
+
|
|
282
|
+
if not session["running"]:
|
|
283
|
+
return f"Process in session '{id}' is not running"
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Use communicate with timeout
|
|
287
|
+
stdout, stderr = process.communicate(timeout=seconds)
|
|
288
|
+
|
|
289
|
+
if stdout:
|
|
290
|
+
stdout_str = (
|
|
291
|
+
stdout.decode('utf-8')
|
|
292
|
+
if isinstance(stdout, bytes)
|
|
293
|
+
else stdout
|
|
294
|
+
)
|
|
295
|
+
session["output"] += stdout_str
|
|
296
|
+
if stderr:
|
|
297
|
+
stderr_str = (
|
|
298
|
+
stderr.decode('utf-8')
|
|
299
|
+
if isinstance(stderr, bytes)
|
|
300
|
+
else stderr
|
|
301
|
+
)
|
|
302
|
+
session["output"] += f"\nErrors:\n{stderr_str}"
|
|
303
|
+
|
|
304
|
+
session["running"] = False
|
|
305
|
+
return (
|
|
306
|
+
f"Process completed in session '{id}'. "
|
|
307
|
+
f"Output: {session['output']}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
except subprocess.TimeoutExpired:
|
|
311
|
+
return (
|
|
312
|
+
f"Process in session '{id}' is still running "
|
|
313
|
+
f"after {seconds} seconds"
|
|
314
|
+
)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Error waiting for process: {e}")
|
|
317
|
+
return f"Error waiting for process: {e!s}"
|
|
318
|
+
|
|
319
|
+
def shell_write_to_process(
|
|
320
|
+
self, id: str, input: str, press_enter: bool
|
|
321
|
+
) -> str:
|
|
322
|
+
r"""Write input to a running process in a specified shell session.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
id (str): Unique identifier of the target shell session.
|
|
326
|
+
input (str): Input content to write to the process.
|
|
327
|
+
press_enter (bool): Whether to press Enter key after input.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
str: Status message indicating whether the input was sent.
|
|
331
|
+
"""
|
|
332
|
+
if id not in self.shell_sessions:
|
|
333
|
+
return f"Shell session not found: {id}"
|
|
334
|
+
|
|
335
|
+
session = self.shell_sessions[id]
|
|
336
|
+
process = session.get("process")
|
|
337
|
+
|
|
338
|
+
if process is None:
|
|
339
|
+
return f"No active process in session '{id}'"
|
|
340
|
+
|
|
341
|
+
if not session["running"] or process.poll() is not None:
|
|
342
|
+
return f"Process in session '{id}' is not running"
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
if not process.stdin or process.stdin.closed:
|
|
346
|
+
return (
|
|
347
|
+
f"Cannot write to process in session '{id}': "
|
|
348
|
+
f"stdin is closed"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if press_enter:
|
|
352
|
+
input = input + "\n"
|
|
353
|
+
|
|
354
|
+
# Write bytes to stdin
|
|
355
|
+
process.stdin.write(input.encode('utf-8'))
|
|
356
|
+
process.stdin.flush()
|
|
357
|
+
|
|
358
|
+
return f"Input sent to process in session '{id}'"
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error(f"Error writing to process: {e}")
|
|
361
|
+
return f"Error writing to process: {e!s}"
|
|
362
|
+
|
|
363
|
+
def shell_kill_process(self, id: str) -> str:
|
|
364
|
+
r"""Terminate a running process in a specified shell session.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
id (str): Unique identifier of the target shell session.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
str: Status message indicating whether the process was terminated.
|
|
371
|
+
"""
|
|
372
|
+
if id not in self.shell_sessions:
|
|
373
|
+
return f"Shell session not found: {id}"
|
|
374
|
+
|
|
375
|
+
session = self.shell_sessions[id]
|
|
376
|
+
process = session.get("process")
|
|
377
|
+
|
|
378
|
+
if process is None:
|
|
379
|
+
return f"No active process in session '{id}'"
|
|
380
|
+
|
|
381
|
+
if not session["running"] or process.poll() is not None:
|
|
382
|
+
return f"Process in session '{id}' is not running"
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
# Clean up process resources before termination
|
|
386
|
+
if process.stdin and not process.stdin.closed:
|
|
387
|
+
process.stdin.close()
|
|
388
|
+
|
|
389
|
+
process.terminate()
|
|
390
|
+
try:
|
|
391
|
+
process.wait(timeout=5)
|
|
392
|
+
except subprocess.TimeoutExpired:
|
|
393
|
+
logger.warning(
|
|
394
|
+
f"Process in session '{id}' did not terminate gracefully"
|
|
395
|
+
f", forcing kill"
|
|
396
|
+
)
|
|
397
|
+
process.kill()
|
|
398
|
+
|
|
399
|
+
session["running"] = False
|
|
400
|
+
return f"Process in session '{id}' has been terminated"
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.error(f"Error killing process: {e}")
|
|
403
|
+
return f"Error killing process: {e!s}"
|
|
404
|
+
|
|
405
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
406
|
+
r"""Returns a list of FunctionTool objects representing the functions
|
|
407
|
+
in the toolkit.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
List[FunctionTool]: A list of FunctionTool objects representing the
|
|
411
|
+
functions in the toolkit.
|
|
412
|
+
"""
|
|
413
|
+
return [
|
|
414
|
+
FunctionTool(self.file_find_in_content),
|
|
415
|
+
FunctionTool(self.file_find_by_name),
|
|
416
|
+
FunctionTool(self.shell_exec),
|
|
417
|
+
FunctionTool(self.shell_view),
|
|
418
|
+
FunctionTool(self.shell_wait),
|
|
419
|
+
FunctionTool(self.shell_write_to_process),
|
|
420
|
+
FunctionTool(self.shell_kill_process),
|
|
421
|
+
]
|
camel/toolkits/web_toolkit.py
CHANGED
|
@@ -157,16 +157,15 @@ def _parse_json_output(text: str) -> Dict[str, Any]:
|
|
|
157
157
|
if triple_quotes_match:
|
|
158
158
|
text = triple_quotes_match.group(1).strip()
|
|
159
159
|
|
|
160
|
-
text = text.replace("`", '"')
|
|
161
|
-
|
|
162
160
|
try:
|
|
163
161
|
return json.loads(text)
|
|
164
162
|
except json.JSONDecodeError:
|
|
165
163
|
try:
|
|
166
|
-
fixed_text = re.sub(
|
|
164
|
+
fixed_text = re.sub(
|
|
165
|
+
r'`([^`]*?)`(?=\s*[:,\[\]{}]|$)', r'"\1"', text
|
|
166
|
+
)
|
|
167
167
|
return json.loads(fixed_text)
|
|
168
168
|
except json.JSONDecodeError:
|
|
169
|
-
# Try to extract key fields
|
|
170
169
|
result = {}
|
|
171
170
|
try:
|
|
172
171
|
bool_pattern = r'"(\w+)"\s*:\s*(true|false)'
|
|
@@ -986,7 +985,7 @@ Here are an example of the output:
|
|
|
986
985
|
{{
|
|
987
986
|
"observation": [IMAGE_DESCRIPTION],
|
|
988
987
|
"reasoning": [YOUR_REASONING],
|
|
989
|
-
"action_code":
|
|
988
|
+
"action_code": "fill_input_id([ID], [TEXT])"
|
|
990
989
|
}}
|
|
991
990
|
|
|
992
991
|
Here are some tips for you:
|