Skip to content

Commit 974dd64

Browse files
dandansamaxzechengzlightaime
authored
Add code interpreter module (#400)
Co-authored-by: Zecheng Zhang <[email protected]> Co-authored-by: lig <[email protected]>
1 parent 067b558 commit 974dd64

17 files changed

+705
-178
lines changed

camel/agents/embodied_agent.py

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313
# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. ===========
14-
from typing import Any, Dict, List, Optional
14+
from typing import Any, List, Optional
1515

1616
from colorama import Fore
1717

18-
from camel.agents import BaseToolAgent, ChatAgent, HuggingFaceToolAgent
18+
from camel.agents import BaseToolAgent, ChatAgent
19+
from camel.interpreters import (
20+
BaseInterpreter,
21+
InternalPythonInterpreter,
22+
SubprocessInterpreter,
23+
)
1924
from camel.messages import BaseMessage
2025
from camel.responses import ChatAgentResponse
2126
from camel.types import ModelType
22-
from camel.utils import PythonInterpreter, print_text_animated
27+
from camel.utils import print_text_animated
2328

2429

2530
class EmbodiedAgent(ChatAgent):
@@ -34,8 +39,13 @@ class EmbodiedAgent(ChatAgent):
3439
message_window_size (int, optional): The maximum number of previous
3540
messages to include in the context window. If `None`, no windowing
3641
is performed. (default: :obj:`None`)
37-
action_space (List[Any], optional): The action space for the embodied
38-
agent. (default: :obj:`None`)
42+
tool_agents (List[BaseToolAgent], optional): The tools agents to use in
43+
the embodied agent. (default: :obj:`None`)
44+
code_interpreter (BaseInterpreter, optional): The code interpreter to
45+
execute codes. If `code_interpreter` and `tool_agent` are both
46+
`None`, default to `SubProcessInterpreter`. If `code_interpreter`
47+
is `None` and `tool_agents` is not `None`, default to
48+
`InternalPythonInterpreter`. (default: :obj:`None`)
3949
verbose (bool, optional): Whether to print the critic's messages.
4050
logger_color (Any): The color of the logger displayed to the user.
4151
(default: :obj:`Fore.MAGENTA`)
@@ -47,18 +57,22 @@ def __init__(
4757
model_type: ModelType = ModelType.GPT_4,
4858
model_config: Optional[Any] = None,
4959
message_window_size: Optional[int] = None,
50-
action_space: Optional[List[BaseToolAgent]] = None,
60+
tool_agents: Optional[List[BaseToolAgent]] = None,
61+
code_interpreter: Optional[BaseInterpreter] = None,
5162
verbose: bool = False,
5263
logger_color: Any = Fore.MAGENTA,
5364
) -> None:
54-
default_action_space = [
55-
HuggingFaceToolAgent('hugging_face_tool_agent',
56-
model_type=model_type.value),
57-
]
58-
self.action_space = action_space or default_action_space
59-
action_space_prompt = self.get_action_space_prompt()
60-
system_message.content = system_message.content.format(
61-
action_space=action_space_prompt)
65+
self.tool_agents = tool_agents
66+
self.code_interpreter: BaseInterpreter
67+
if code_interpreter is not None:
68+
self.code_interpreter = code_interpreter
69+
elif self.tool_agents:
70+
self.code_interpreter = InternalPythonInterpreter()
71+
else:
72+
self.code_interpreter = SubprocessInterpreter()
73+
74+
if self.tool_agents:
75+
system_message = self._set_tool_agents(system_message)
6276
self.verbose = verbose
6377
self.logger_color = logger_color
6478
super().__init__(
@@ -68,16 +82,41 @@ def __init__(
6882
message_window_size=message_window_size,
6983
)
7084

71-
def get_action_space_prompt(self) -> str:
85+
def _set_tool_agents(self, system_message: BaseMessage) -> BaseMessage:
86+
action_space_prompt = self._get_tool_agents_prompt()
87+
result_message = system_message.create_new_instance(
88+
content=system_message.content.format(
89+
action_space=action_space_prompt))
90+
if self.tool_agents is not None:
91+
self.code_interpreter.update_action_space(
92+
{tool.name: tool
93+
for tool in self.tool_agents})
94+
return result_message
95+
96+
def _get_tool_agents_prompt(self) -> str:
7297
r"""Returns the action space prompt.
7398
7499
Returns:
75100
str: The action space prompt.
76101
"""
77-
return "\n".join([
78-
f"*** {action.name} ***:\n {action.description}"
79-
for action in self.action_space
80-
])
102+
if self.tool_agents is not None:
103+
return "\n".join([
104+
f"*** {tool.name} ***:\n {tool.description}"
105+
for tool in self.tool_agents
106+
])
107+
else:
108+
return ""
109+
110+
def get_tool_agent_names(self) -> List[str]:
111+
r"""Returns the names of tool agents.
112+
113+
Returns:
114+
List[str]: The names of tool agents.
115+
"""
116+
if self.tool_agents is not None:
117+
return [tool.name for tool in self.tool_agents]
118+
else:
119+
return []
81120

82121
def step(
83122
self,
@@ -111,28 +150,24 @@ def step(
111150

112151
if len(explanations) > len(codes):
113152
print_text_animated(self.logger_color +
114-
f"> Explanation:\n{explanations}")
153+
f"> Explanation:\n{explanations[-1]}")
115154

116155
content = response.msg.content
117156

118157
if codes is not None:
119-
content = "\n> Executed Results:"
120-
action_space: Dict[str, Any] = {
121-
action.name: action
122-
for action in self.action_space
123-
}
124-
action_space.update({"print": print, "enumerate": enumerate})
125-
interpreter = PythonInterpreter(action_space=action_space)
126-
for block_idx, code in enumerate(codes):
127-
executed_outputs, _ = code.execute(interpreter)
128-
content += (f"Executing code block {block_idx}:\n"
129-
f" - execution output:\n{executed_outputs}\n"
130-
f" - Local variables:\n{interpreter.state}\n")
131-
content += "*" * 50 + "\n"
158+
try:
159+
content = "\n> Executed Results:\n"
160+
for block_idx, code in enumerate(codes):
161+
executed_output = self.code_interpreter.run(
162+
code, code.code_type)
163+
content += (f"Executing code block {block_idx}: {{\n" +
164+
executed_output + "}\n")
165+
except InterruptedError as e:
166+
content = (f"\n> Running code fail: {e}\n"
167+
"Please regenerate the code.")
132168

133169
# TODO: Handle errors
134-
content = input_message.content + (Fore.RESET +
135-
f"\n> Embodied Actions:\n{content}")
170+
content = input_message.content + f"\n> Embodied Actions:\n{content}"
136171
message = BaseMessage(input_message.role_name, input_message.role_type,
137172
input_message.meta_dict, content)
138173
return ChatAgentResponse([message], response.terminated, response.info)

camel/interpreters/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========
14+
15+
from .base import BaseInterpreter
16+
from .interpreter_error import InterpreterError
17+
from .internal_python_interpreter import InternalPythonInterpreter
18+
from .subprocess_interpreter import SubprocessInterpreter
19+
20+
__all__ = [
21+
'BaseInterpreter',
22+
'InterpreterError',
23+
'InternalPythonInterpreter',
24+
'SubprocessInterpreter',
25+
]

camel/interpreters/base.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========
14+
from abc import ABC, abstractmethod
15+
from typing import Any, Dict, List
16+
17+
18+
class BaseInterpreter(ABC):
19+
r"""An abstract base class for code interpreters."""
20+
21+
@abstractmethod
22+
def run(self, code: str, code_type: str) -> str:
23+
r"""Executes the given code based on its type.
24+
25+
Args:
26+
code (str): The code to be executed.
27+
code_type (str): The type of the code, which must be one of the
28+
types returned by `supported_code_types()`.
29+
30+
Returns:
31+
str: The result of the code execution. If the execution fails, this
32+
should include sufficient information to diagnose and correct
33+
the issue.
34+
35+
Raises:
36+
InterpreterError: If the code execution encounters errors that
37+
could be resolved by modifying or regenerating the code.
38+
"""
39+
pass
40+
41+
@abstractmethod
42+
def supported_code_types(self) -> List[str]:
43+
r"""Provides supported code types by the interpreter."""
44+
pass
45+
46+
@abstractmethod
47+
def update_action_space(self, action_space: Dict[str, Any]) -> None:
48+
r"""Updates action space for *python* interpreter"""
49+
pass

camel/utils/python_interpreter.py renamed to camel/interpreters/internal_python_interpreter.py

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,14 @@
1717
import typing
1818
from typing import Any, Dict, List, Optional
1919

20-
21-
class InterpreterError(ValueError):
22-
r"""An error raised when the interpreter cannot evaluate a Python
23-
expression, due to syntax error or unsupported operations.
24-
"""
25-
26-
pass
20+
from camel.interpreters import BaseInterpreter, InterpreterError
2721

2822

29-
class PythonInterpreter():
23+
class InternalPythonInterpreter(BaseInterpreter):
3024
r"""A customized python interpreter to control the execution of
3125
LLM-generated codes. The interpreter makes sure the code can only execute
3226
functions given in action space and import white list. It also supports
33-
fuzzy variable matching to reveive uncertain input variable name.
27+
fuzzy variable matching to retrieve uncertain input variable name.
3428
3529
.. highlight:: none
3630
@@ -64,35 +58,88 @@ class PythonInterpreter():
6458
Modifications copyright (C) 2023 CAMEL-AI.org
6559
6660
Args:
67-
action_space (Dict[str, Any]): A dictionary that maps action names to
68-
their corresponding functions or objects. The interpreter can only
69-
execute functions that are either directly listed in this
61+
action_space (Dict[str, Any], optional): A dictionary that maps action
62+
names to their corresponding functions or objects. The interpreter
63+
can only execute functions that are either directly listed in this
7064
dictionary or are member functions of objects listed in this
7165
dictionary. The concept of :obj:`action_space` is derived from
7266
EmbodiedAgent, representing the actions that an agent is capable of
73-
performing.
74-
import_white_list (Optional[List[str]], optional): A list that stores
67+
performing. If `None`, set to empty dict. (default: :obj:`None`)
68+
import_white_list (List[str], optional): A list that stores
7569
the Python modules or functions that can be imported in the code.
7670
All submodules and functions of the modules listed in this list are
7771
importable. Any other import statements will be rejected. The
7872
module and its submodule or function name are separated by a period
7973
(:obj:`.`). (default: :obj:`None`)
74+
unsafe_mode (bool, optional): If `True`, the interpreter runs the code
75+
by `eval()` without any security check. (default: :obj:`False`)
8076
raise_error (bool, optional): Raise error if the interpreter fails.
77+
(default: :obj:`False`)
8178
"""
8279

83-
def __init__(self, action_space: Dict[str, Any],
84-
import_white_list: Optional[List[str]] = None,
85-
raise_error: bool = False) -> None:
86-
self.action_space = action_space
80+
_CODE_TYPES = ["python", "py", "python3", "python2"]
81+
82+
def __init__(
83+
self,
84+
action_space: Optional[Dict[str, Any]] = None,
85+
import_white_list: Optional[List[str]] = None,
86+
unsafe_mode: bool = False,
87+
raise_error: bool = False,
88+
) -> None:
89+
self.action_space = action_space or dict()
8790
self.state = self.action_space.copy()
88-
self.fuzz_state: Dict[str, Any] = {}
89-
self.import_white_list = import_white_list or []
91+
self.fuzz_state: Dict[str, Any] = dict()
92+
self.import_white_list = import_white_list or list()
9093
self.raise_error = raise_error
94+
self.unsafe_mode = unsafe_mode
95+
96+
def run(self, code: str, code_type: str) -> str:
97+
r"""Executes the given code with specified code type in the
98+
interpreter.
99+
100+
This method takes a string of code and its type, checks if the code
101+
type is supported, and then executes the code. If `unsafe_mode` is
102+
set to `False`, the code is executed in a controlled environment using
103+
the `execute` method. If `unsafe_mode` is `True`, the code is executed
104+
using `eval()` with the action space as the global context. An
105+
`InterpreterError` is raised if the code type is unsupported or if any
106+
runtime error occurs during execution.
107+
108+
Args:
109+
code (str): The python code to be executed.
110+
code_type (str): The type of the code, which should be one of the
111+
supported code types (`python`, `py`, `python3`, `python2`).
112+
113+
114+
Returns:
115+
str: The string representation of the output of the executed code.
116+
117+
Raises:
118+
InterpreterError: If the `code_type` is not supported or if any
119+
runtime error occurs during the execution of the code.
120+
"""
121+
if code_type not in self._CODE_TYPES:
122+
raise InterpreterError(
123+
f"Unsupported code type {code_type}. "
124+
f"`{self.__class__.__name__}` only supports "
125+
f"{', '.join(self._CODE_TYPES)}.")
126+
if not self.unsafe_mode:
127+
return str(self.execute(code))
128+
else:
129+
return str(eval(code, self.action_space))
130+
131+
def update_action_space(self, action_space: Dict[str, Any]) -> None:
132+
r"""Updates action space for *python* interpreter."""
133+
self.action_space.update(action_space)
134+
135+
def supported_code_types(self) -> List[str]:
136+
r"""Provides supported code types by the interpreter."""
137+
return self._CODE_TYPES
91138

92139
def execute(self, code: str, state: Optional[Dict[str, Any]] = None,
93140
fuzz_state: Optional[Dict[str, Any]] = None,
94141
keep_state: bool = True) -> Any:
95-
r""" Execute the input python codes in a security environment.
142+
r"""Execute the input python codes in a security environment.
96143
97144
Args:
98145
code (str): Generated python code to be executed.
@@ -154,7 +201,7 @@ def execute(self, code: str, state: Optional[Dict[str, Any]] = None,
154201
return result
155202

156203
def clear_state(self) -> None:
157-
r"""Initialize :obj:`state` and :obj:`fuzz_state`"""
204+
r"""Initialize :obj:`state` and :obj:`fuzz_state`."""
158205
self.state = self.action_space.copy()
159206
self.fuzz_state = {}
160207

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# =========== Copyright 2023 @ 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 @ CAMEL-AI.org. All Rights Reserved. ===========
14+
15+
16+
class InterpreterError(Exception):
17+
r"""Exception raised for errors that can be solved by regenerating code """
18+
pass

0 commit comments

Comments
 (0)