camel-ai 0.2.9__py3-none-any.whl → 0.2.11__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.

Files changed (242) hide show
  1. camel/__init__.py +10 -5
  2. camel/agents/__init__.py +4 -4
  3. camel/agents/base.py +4 -4
  4. camel/agents/chat_agent.py +106 -42
  5. camel/agents/critic_agent.py +4 -4
  6. camel/agents/deductive_reasoner_agent.py +8 -5
  7. camel/agents/embodied_agent.py +4 -4
  8. camel/agents/knowledge_graph_agent.py +4 -4
  9. camel/agents/role_assignment_agent.py +4 -4
  10. camel/agents/search_agent.py +4 -4
  11. camel/agents/task_agent.py +4 -4
  12. camel/agents/tool_agents/__init__.py +4 -4
  13. camel/agents/tool_agents/base.py +4 -4
  14. camel/agents/tool_agents/hugging_face_tool_agent.py +4 -4
  15. camel/bots/__init__.py +4 -4
  16. camel/bots/discord_app.py +4 -4
  17. camel/bots/slack/__init__.py +4 -4
  18. camel/bots/slack/models.py +4 -4
  19. camel/bots/slack/slack_app.py +4 -4
  20. camel/bots/telegram_bot.py +4 -4
  21. camel/configs/__init__.py +13 -4
  22. camel/configs/anthropic_config.py +4 -4
  23. camel/configs/base_config.py +4 -4
  24. camel/configs/cohere_config.py +76 -0
  25. camel/configs/deepseek_config.py +134 -0
  26. camel/configs/gemini_config.py +85 -127
  27. camel/configs/groq_config.py +4 -4
  28. camel/configs/litellm_config.py +4 -4
  29. camel/configs/mistral_config.py +4 -7
  30. camel/configs/nvidia_config.py +70 -0
  31. camel/configs/ollama_config.py +4 -4
  32. camel/configs/openai_config.py +32 -7
  33. camel/configs/qwen_config.py +4 -4
  34. camel/configs/reka_config.py +4 -4
  35. camel/configs/samba_config.py +4 -4
  36. camel/configs/togetherai_config.py +4 -4
  37. camel/configs/vllm_config.py +14 -5
  38. camel/configs/yi_config.py +4 -4
  39. camel/configs/zhipuai_config.py +4 -4
  40. camel/embeddings/__init__.py +6 -4
  41. camel/embeddings/base.py +4 -4
  42. camel/embeddings/mistral_embedding.py +4 -4
  43. camel/embeddings/openai_compatible_embedding.py +91 -0
  44. camel/embeddings/openai_embedding.py +4 -4
  45. camel/embeddings/sentence_transformers_embeddings.py +4 -4
  46. camel/embeddings/vlm_embedding.py +8 -5
  47. camel/generators.py +4 -4
  48. camel/human.py +4 -4
  49. camel/interpreters/__init__.py +4 -4
  50. camel/interpreters/base.py +4 -4
  51. camel/interpreters/docker_interpreter.py +11 -6
  52. camel/interpreters/internal_python_interpreter.py +4 -4
  53. camel/interpreters/interpreter_error.py +4 -4
  54. camel/interpreters/ipython_interpreter.py +4 -4
  55. camel/interpreters/subprocess_interpreter.py +11 -6
  56. camel/loaders/__init__.py +4 -4
  57. camel/loaders/apify_reader.py +4 -4
  58. camel/loaders/base_io.py +4 -4
  59. camel/loaders/chunkr_reader.py +4 -4
  60. camel/loaders/firecrawl_reader.py +4 -7
  61. camel/loaders/jina_url_reader.py +4 -4
  62. camel/loaders/unstructured_io.py +4 -4
  63. camel/logger.py +112 -0
  64. camel/memories/__init__.py +4 -4
  65. camel/memories/agent_memories.py +4 -4
  66. camel/memories/base.py +4 -4
  67. camel/memories/blocks/__init__.py +4 -4
  68. camel/memories/blocks/chat_history_block.py +4 -4
  69. camel/memories/blocks/vectordb_block.py +4 -4
  70. camel/memories/context_creators/__init__.py +4 -4
  71. camel/memories/context_creators/score_based.py +4 -4
  72. camel/memories/records.py +4 -4
  73. camel/messages/__init__.py +20 -4
  74. camel/messages/base.py +118 -11
  75. camel/messages/conversion/__init__.py +31 -0
  76. camel/messages/conversion/alpaca.py +122 -0
  77. camel/messages/conversion/conversation_models.py +178 -0
  78. camel/messages/conversion/sharegpt/__init__.py +20 -0
  79. camel/messages/conversion/sharegpt/function_call_formatter.py +49 -0
  80. camel/messages/conversion/sharegpt/hermes/__init__.py +19 -0
  81. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +128 -0
  82. camel/messages/func_message.py +50 -4
  83. camel/models/__init__.py +13 -4
  84. camel/models/anthropic_model.py +4 -4
  85. camel/models/azure_openai_model.py +4 -4
  86. camel/models/base_model.py +4 -4
  87. camel/models/cohere_model.py +282 -0
  88. camel/models/deepseek_model.py +139 -0
  89. camel/models/gemini_model.py +61 -146
  90. camel/models/groq_model.py +4 -4
  91. camel/models/litellm_model.py +4 -4
  92. camel/models/mistral_model.py +4 -4
  93. camel/models/model_factory.py +13 -4
  94. camel/models/model_manager.py +212 -0
  95. camel/models/nemotron_model.py +4 -4
  96. camel/models/nvidia_model.py +141 -0
  97. camel/models/ollama_model.py +4 -4
  98. camel/models/openai_audio_models.py +4 -4
  99. camel/models/openai_compatible_model.py +4 -4
  100. camel/models/openai_model.py +43 -4
  101. camel/models/qwen_model.py +4 -4
  102. camel/models/reka_model.py +4 -4
  103. camel/models/samba_model.py +6 -5
  104. camel/models/stub_model.py +4 -4
  105. camel/models/togetherai_model.py +4 -4
  106. camel/models/vllm_model.py +4 -4
  107. camel/models/yi_model.py +4 -4
  108. camel/models/zhipuai_model.py +4 -4
  109. camel/personas/__init__.py +17 -0
  110. camel/personas/persona.py +103 -0
  111. camel/personas/persona_hub.py +293 -0
  112. camel/prompts/__init__.py +6 -4
  113. camel/prompts/ai_society.py +4 -4
  114. camel/prompts/base.py +4 -4
  115. camel/prompts/code.py +4 -4
  116. camel/prompts/evaluation.py +4 -4
  117. camel/prompts/generate_text_embedding_data.py +4 -4
  118. camel/prompts/image_craft.py +4 -4
  119. camel/prompts/misalignment.py +4 -4
  120. camel/prompts/multi_condition_image_craft.py +4 -4
  121. camel/prompts/object_recognition.py +4 -4
  122. camel/prompts/persona_hub.py +61 -0
  123. camel/prompts/prompt_templates.py +4 -4
  124. camel/prompts/role_description_prompt_template.py +4 -4
  125. camel/prompts/solution_extraction.py +4 -4
  126. camel/prompts/task_prompt_template.py +4 -4
  127. camel/prompts/translation.py +4 -4
  128. camel/prompts/video_description_prompt.py +4 -4
  129. camel/responses/__init__.py +4 -4
  130. camel/responses/agent_responses.py +4 -4
  131. camel/retrievers/__init__.py +4 -4
  132. camel/retrievers/auto_retriever.py +4 -4
  133. camel/retrievers/base.py +4 -4
  134. camel/retrievers/bm25_retriever.py +4 -4
  135. camel/retrievers/cohere_rerank_retriever.py +7 -9
  136. camel/retrievers/vector_retriever.py +26 -9
  137. camel/runtime/__init__.py +29 -0
  138. camel/runtime/api.py +93 -0
  139. camel/runtime/base.py +45 -0
  140. camel/runtime/configs.py +56 -0
  141. camel/runtime/docker_runtime.py +404 -0
  142. camel/runtime/llm_guard_runtime.py +199 -0
  143. camel/runtime/remote_http_runtime.py +204 -0
  144. camel/runtime/utils/__init__.py +20 -0
  145. camel/runtime/utils/function_risk_toolkit.py +58 -0
  146. camel/runtime/utils/ignore_risk_toolkit.py +72 -0
  147. camel/schemas/__init__.py +17 -0
  148. camel/schemas/base.py +45 -0
  149. camel/schemas/openai_converter.py +116 -0
  150. camel/societies/__init__.py +4 -4
  151. camel/societies/babyagi_playing.py +8 -5
  152. camel/societies/role_playing.py +4 -4
  153. camel/societies/workforce/__init__.py +4 -4
  154. camel/societies/workforce/base.py +4 -4
  155. camel/societies/workforce/prompts.py +4 -4
  156. camel/societies/workforce/role_playing_worker.py +4 -4
  157. camel/societies/workforce/single_agent_worker.py +4 -4
  158. camel/societies/workforce/task_channel.py +4 -4
  159. camel/societies/workforce/utils.py +4 -4
  160. camel/societies/workforce/worker.py +4 -4
  161. camel/societies/workforce/workforce.py +7 -7
  162. camel/storages/__init__.py +4 -4
  163. camel/storages/graph_storages/__init__.py +4 -4
  164. camel/storages/graph_storages/base.py +4 -4
  165. camel/storages/graph_storages/graph_element.py +4 -4
  166. camel/storages/graph_storages/nebula_graph.py +4 -4
  167. camel/storages/graph_storages/neo4j_graph.py +4 -4
  168. camel/storages/key_value_storages/__init__.py +4 -4
  169. camel/storages/key_value_storages/base.py +4 -4
  170. camel/storages/key_value_storages/in_memory.py +4 -4
  171. camel/storages/key_value_storages/json.py +4 -4
  172. camel/storages/key_value_storages/redis.py +4 -4
  173. camel/storages/object_storages/__init__.py +4 -4
  174. camel/storages/object_storages/amazon_s3.py +4 -4
  175. camel/storages/object_storages/azure_blob.py +4 -4
  176. camel/storages/object_storages/base.py +4 -4
  177. camel/storages/object_storages/google_cloud.py +4 -4
  178. camel/storages/vectordb_storages/__init__.py +4 -4
  179. camel/storages/vectordb_storages/base.py +4 -4
  180. camel/storages/vectordb_storages/milvus.py +4 -4
  181. camel/storages/vectordb_storages/qdrant.py +4 -4
  182. camel/tasks/__init__.py +4 -4
  183. camel/tasks/task.py +4 -4
  184. camel/tasks/task_prompt.py +4 -4
  185. camel/terminators/__init__.py +4 -4
  186. camel/terminators/base.py +4 -4
  187. camel/terminators/response_terminator.py +4 -4
  188. camel/terminators/token_limit_terminator.py +4 -4
  189. camel/toolkits/__init__.py +16 -17
  190. camel/toolkits/arxiv_toolkit.py +4 -4
  191. camel/toolkits/ask_news_toolkit.py +7 -18
  192. camel/toolkits/base.py +4 -4
  193. camel/toolkits/code_execution.py +57 -10
  194. camel/toolkits/dalle_toolkit.py +4 -7
  195. camel/toolkits/data_commons_toolkit.py +4 -4
  196. camel/toolkits/function_tool.py +220 -69
  197. camel/toolkits/github_toolkit.py +4 -4
  198. camel/toolkits/google_maps_toolkit.py +4 -4
  199. camel/toolkits/google_scholar_toolkit.py +4 -4
  200. camel/toolkits/human_toolkit.py +53 -0
  201. camel/toolkits/linkedin_toolkit.py +4 -4
  202. camel/toolkits/math_toolkit.py +4 -7
  203. camel/toolkits/meshy_toolkit.py +185 -0
  204. camel/toolkits/notion_toolkit.py +4 -4
  205. camel/toolkits/open_api_specs/biztoc/__init__.py +4 -4
  206. camel/toolkits/open_api_specs/coursera/__init__.py +4 -4
  207. camel/toolkits/open_api_specs/create_qr_code/__init__.py +4 -4
  208. camel/toolkits/open_api_specs/klarna/__init__.py +4 -4
  209. camel/toolkits/open_api_specs/nasa_apod/__init__.py +4 -4
  210. camel/toolkits/open_api_specs/outschool/__init__.py +4 -4
  211. camel/toolkits/open_api_specs/outschool/paths/__init__.py +4 -4
  212. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +4 -4
  213. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +4 -4
  214. camel/toolkits/open_api_specs/security_config.py +4 -4
  215. camel/toolkits/open_api_specs/speak/__init__.py +4 -4
  216. camel/toolkits/open_api_specs/web_scraper/__init__.py +4 -4
  217. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +4 -4
  218. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +4 -4
  219. camel/toolkits/open_api_toolkit.py +4 -4
  220. camel/toolkits/reddit_toolkit.py +4 -4
  221. camel/toolkits/retrieval_toolkit.py +4 -4
  222. camel/toolkits/search_toolkit.py +49 -29
  223. camel/toolkits/slack_toolkit.py +4 -4
  224. camel/toolkits/twitter_toolkit.py +13 -13
  225. camel/toolkits/video_toolkit.py +211 -0
  226. camel/toolkits/weather_toolkit.py +4 -7
  227. camel/toolkits/whatsapp_toolkit.py +6 -6
  228. camel/types/__init__.py +6 -4
  229. camel/types/enums.py +118 -15
  230. camel/types/openai_types.py +6 -4
  231. camel/types/unified_model_type.py +9 -4
  232. camel/utils/__init__.py +35 -33
  233. camel/utils/async_func.py +4 -4
  234. camel/utils/commons.py +26 -9
  235. camel/utils/constants.py +4 -4
  236. camel/utils/response_format.py +63 -0
  237. camel/utils/token_counting.py +8 -5
  238. {camel_ai-0.2.9.dist-info → camel_ai-0.2.11.dist-info}/METADATA +108 -56
  239. camel_ai-0.2.11.dist-info/RECORD +252 -0
  240. camel_ai-0.2.9.dist-info/RECORD +0 -215
  241. {camel_ai-0.2.9.dist-info → camel_ai-0.2.11.dist-info}/LICENSE +0 -0
  242. {camel_ai-0.2.9.dist-info → camel_ai-0.2.11.dist-info}/WHEEL +0 -0
@@ -0,0 +1,404 @@
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 io
15
+ import json
16
+ import logging
17
+ import os
18
+ import tarfile
19
+ import time
20
+ from functools import wraps
21
+ from pathlib import Path
22
+ from random import randint
23
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
24
+
25
+ import requests
26
+ from pydantic import BaseModel
27
+ from tqdm import tqdm
28
+
29
+ from camel.runtime import BaseRuntime, TaskConfig
30
+ from camel.toolkits import FunctionTool
31
+
32
+ if TYPE_CHECKING:
33
+ from docker.models.containers import Container
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class DockerRuntime(BaseRuntime):
39
+ r"""A class representing a runtime environment using Docker.
40
+ This class automatically wraps functions to be executed
41
+ in a Docker container.
42
+
43
+ Args:
44
+ image (str): The name of the Docker image to use for the runtime.
45
+ port (int): The port number to use for the runtime API. (default::obj:
46
+ `8000`)
47
+ remove (bool): Whether to remove the container after stopping it. '
48
+ (default::obj: `True`)
49
+ kwargs (dict): Additional keyword arguments to pass to the
50
+ Docker client.
51
+ """
52
+
53
+ def __init__(
54
+ self, image: str, port: int = 8000, remove: bool = True, **kwargs
55
+ ):
56
+ super().__init__()
57
+
58
+ import docker
59
+
60
+ self.client = docker.from_env()
61
+ self.container: Optional[Container] = None
62
+
63
+ api_path = Path(__file__).parent / "api.py"
64
+ self.mounts: Dict[Path, Path] = dict()
65
+ self.cp: Dict[Path, Path] = {api_path: Path("/home")}
66
+ self.entrypoint: Dict[str, str] = dict()
67
+ self.tasks: List[TaskConfig] = []
68
+
69
+ self.docker_config = kwargs
70
+ self.image = image
71
+ self.port = port if port > 0 else randint(10000, 20000)
72
+ self.remove = remove
73
+
74
+ if not self.client.images.list(name=self.image):
75
+ logger.warning(
76
+ f"Image {self.image} not found. Pulling from Docker Hub."
77
+ )
78
+ self.client.images.pull(self.image)
79
+
80
+ def mount(self, path: str, mount_path: str) -> "DockerRuntime":
81
+ r"""Mount a local directory to the container.
82
+
83
+ Args:
84
+ path (str): The local path to mount.
85
+ mount_path (str): The path to mount the local directory to in the
86
+ container.
87
+
88
+ Returns:
89
+ DockerRuntime: The DockerRuntime instance.
90
+ """
91
+
92
+ _path, _mount_path = Path(path), Path(mount_path)
93
+ if not _path.exists():
94
+ raise FileNotFoundError(f"Path {_path} does not exist.")
95
+ if not _path.is_dir():
96
+ raise NotADirectoryError(f"Path {_path} is not a directory.")
97
+ if not _path.is_absolute():
98
+ raise ValueError(f"Path {_path} is not absolute.")
99
+ if not _mount_path.is_absolute():
100
+ raise ValueError(f"Mount path {_mount_path} is not absolute.")
101
+
102
+ self.mounts[_path] = _mount_path
103
+ return self
104
+
105
+ def copy(self, source: str, dest: str) -> "DockerRuntime":
106
+ r"""Copy a file or directory to the container.
107
+
108
+ Args:
109
+ source (str): The local path to the file.
110
+ dest (str): The path to copy the file to in the container.
111
+
112
+ Returns:
113
+ DockerRuntime: The DockerRuntime instance.
114
+ """
115
+ _source, _dest = Path(source), Path(dest)
116
+ if not _source.exists():
117
+ raise FileNotFoundError(f"Source {_source} does not exist.")
118
+
119
+ self.cp[_source] = _dest
120
+ return self
121
+
122
+ def add_task(
123
+ self,
124
+ task: TaskConfig,
125
+ ) -> "DockerRuntime":
126
+ r"""Add a task to run a command inside the container when building.
127
+ Similar to `docker exec`.
128
+
129
+ Args:
130
+ task (TaskConfig): The configuration for the task.
131
+
132
+ Returns:
133
+ DockerRuntime: The DockerRuntime instance.
134
+ """
135
+ self.tasks.append(task)
136
+ return self
137
+
138
+ def exec_run(
139
+ self,
140
+ task: TaskConfig,
141
+ ) -> Any:
142
+ r"""Run a command inside this container. Similar to `docker exec`.
143
+
144
+ Args:
145
+ task (TaskConfig): The configuration for the task.
146
+
147
+ Returns:
148
+ (ExecResult): A tuple of (exit_code, output)
149
+ exit_code: (int):
150
+ Exit code for the executed command or `None` if
151
+ either `stream` or `socket` is `True`.
152
+ output: (generator, bytes, or tuple):
153
+ If `stream=True`, a generator yielding response chunks.
154
+ If `socket=True`, a socket object for the connection.
155
+ If `demux=True`, a tuple of two bytes: stdout and stderr.
156
+ A bytestring containing response data otherwise.
157
+
158
+ Raises:
159
+ RuntimeError: If the container does not exist.
160
+ """
161
+ if not self.container:
162
+ raise RuntimeError(
163
+ "Container does not exist. Please build the container first."
164
+ )
165
+
166
+ return self.container.exec_run(**task.model_dump())
167
+
168
+ def build(self, time_out: int = 15) -> "DockerRuntime":
169
+ r"""Build the Docker container and start it.
170
+
171
+ Args:
172
+ time_out (int): The number of seconds to wait for the container to
173
+ start. (default::obj: `15`)
174
+
175
+ Returns:
176
+ DockerRuntime: The DockerRuntime instance.
177
+ """
178
+ if self.container:
179
+ logger.warning("Container already exists. Nothing to build.")
180
+ return self
181
+
182
+ import docker
183
+ from docker.types import Mount
184
+
185
+ mounts = []
186
+ for local_path, mount_path in self.mounts.items():
187
+ mounts.append(
188
+ Mount(
189
+ target=str(mount_path), source=str(local_path), type="bind"
190
+ )
191
+ )
192
+
193
+ container_params = {
194
+ "image": self.image,
195
+ "detach": True,
196
+ "mounts": mounts,
197
+ "command": "sleep infinity",
198
+ **self.docker_config,
199
+ }
200
+ container_params["ports"] = {"8000/tcp": self.port}
201
+ try:
202
+ self.container = self.client.containers.create(**container_params)
203
+ except docker.errors.APIError as e:
204
+ raise RuntimeError(f"Failed to create container: {e!s}")
205
+
206
+ try:
207
+ self.container.start()
208
+ # Wait for the container to start
209
+ for _ in range(time_out):
210
+ self.container.reload()
211
+ logger.debug(f"Container status: {self.container.status}")
212
+ if self.container.status == "running":
213
+ break
214
+ time.sleep(1)
215
+
216
+ except docker.errors.APIError as e:
217
+ raise RuntimeError(f"Failed to start container: {e!s}")
218
+
219
+ # Copy files to the container if specified
220
+ for local_path, container_path in self.cp.items():
221
+ logger.info(f"Copying {local_path} to {container_path}")
222
+ try:
223
+ with io.BytesIO() as tar_stream:
224
+ with tarfile.open(fileobj=tar_stream, mode="w") as tar:
225
+ tar.add(
226
+ local_path, arcname=os.path.basename(local_path)
227
+ )
228
+ tar_stream.seek(0)
229
+ self.container.put_archive(
230
+ str(container_path), tar_stream.getvalue()
231
+ )
232
+ except docker.errors.APIError as e:
233
+ raise RuntimeError(
234
+ f"Failed to copy file {local_path} to container: {e!s}"
235
+ )
236
+
237
+ if self.tasks:
238
+ for task in tqdm(self.tasks, desc="Running tasks"):
239
+ self.exec_run(task)
240
+
241
+ exec = ["python3", "api.py", *list(self.entrypoint.values())]
242
+
243
+ self.container.exec_run(exec, workdir="/home", detach=True)
244
+
245
+ logger.info(f"Container started on port {self.port}")
246
+ return self
247
+
248
+ def add( # type: ignore[override]
249
+ self,
250
+ funcs: Union[FunctionTool, List[FunctionTool]],
251
+ entrypoint: str,
252
+ redirect_stdout: bool = False,
253
+ arguments: Optional[Dict[str, Any]] = None,
254
+ ) -> "DockerRuntime":
255
+ r"""Add a function or list of functions to the runtime.
256
+
257
+ Args:
258
+ funcs (Union[FunctionTool, List[FunctionTool]]): The function or
259
+ list of functions to add.
260
+ entrypoint (str): The entrypoint for the function.
261
+ redirect_stdout (bool): Whether to return the stdout of
262
+ the function. (default::obj: `False`)
263
+ arguments (Optional[Dict[str, Any]]): The arguments for the
264
+ function. (default::obj: `None`)
265
+
266
+ Returns:
267
+ DockerRuntime: The DockerRuntime instance.
268
+ """
269
+
270
+ if not isinstance(funcs, list):
271
+ funcs = [funcs]
272
+
273
+ if arguments is not None:
274
+ entrypoint += json.dumps(arguments)
275
+
276
+ for func in funcs:
277
+ inner_func = func.func
278
+
279
+ # Create a wrapper that explicitly binds `func`
280
+ @wraps(inner_func)
281
+ def wrapper(
282
+ *args, func=func, redirect_stdout=redirect_stdout, **kwargs
283
+ ):
284
+ for key, value in kwargs.items():
285
+ if isinstance(value, BaseModel):
286
+ kwargs[key] = value.model_dump()
287
+
288
+ resp = requests.post(
289
+ f"http://localhost:{self.port}/{func.get_function_name()}",
290
+ json=dict(
291
+ args=args,
292
+ kwargs=kwargs,
293
+ redirect_stdout=redirect_stdout,
294
+ ),
295
+ )
296
+ if resp.status_code != 200:
297
+ logger.error(
298
+ f"""ailed to execute function:
299
+ {func.get_function_name()},
300
+ status code: {resp.status_code},
301
+ response: {resp.text}"""
302
+ )
303
+ return {
304
+ "error": f"""Failed to execute function:
305
+ {func.get_function_name()},
306
+ response: {resp.text}"""
307
+ }
308
+ data = resp.json()
309
+ if redirect_stdout:
310
+ print(data["stdout"])
311
+ return json.loads(data["output"])
312
+
313
+ func.func = wrapper
314
+ self.tools_map[func.get_function_name()] = func
315
+ self.entrypoint[func.get_function_name()] = entrypoint
316
+
317
+ return self
318
+
319
+ def reset(self) -> "DockerRuntime":
320
+ r"""Reset the DockerRuntime instance.
321
+
322
+ Returns:
323
+ DockerRuntime: The DockerRuntime instance.
324
+ """
325
+
326
+ return self.stop().build()
327
+
328
+ def stop(self, remove: Optional[bool] = None) -> "DockerRuntime":
329
+ r"""stop the Docker container.
330
+
331
+ Args:
332
+ remove (Optional[bool]): Whether to remove the container
333
+ after stopping it. (default::obj: `None`)
334
+
335
+ Returns:
336
+ DockerRuntime: The DockerRuntime instance.
337
+ """
338
+ if self.container:
339
+ self.container.stop()
340
+ if remove is None:
341
+ remove = self.remove
342
+ if remove:
343
+ logger.info("Removing container.")
344
+ self.container.remove()
345
+ self.container = None
346
+ else:
347
+ logger.warning("No container to stop.")
348
+ return self
349
+
350
+ @property
351
+ def ok(self) -> bool:
352
+ r"""Check if the API Server is running.
353
+
354
+ Returns:
355
+ bool: Whether the API Server is running.
356
+ """
357
+ if not self.container:
358
+ return False
359
+ try:
360
+ _ = requests.get(f"http://localhost:{self.port}")
361
+ return True
362
+ except requests.exceptions.ConnectionError:
363
+ return False
364
+
365
+ def wait(self, timeout: int = 10) -> bool:
366
+ r"""Wait for the API Server to be ready.
367
+
368
+ Args:
369
+ timeout (int): The number of seconds to wait. (default::obj: `10`)
370
+
371
+ Returns:
372
+ bool: Whether the API Server is ready.
373
+ """
374
+ for _ in range(timeout):
375
+ if self.ok:
376
+ return True
377
+ time.sleep(1)
378
+ return False
379
+
380
+ def __enter__(self) -> "DockerRuntime":
381
+ r"""Enter the context manager.
382
+
383
+ Returns:
384
+ DockerRuntime: The DockerRuntime instance.
385
+ """
386
+ if not self.container:
387
+ return self.build()
388
+ logger.warning(
389
+ "Container already exists. Returning existing container."
390
+ )
391
+ return self
392
+
393
+ def __exit__(self, exc_type, exc_val, exc_tb):
394
+ r"""Exit the context manager."""
395
+ self.stop()
396
+
397
+ @property
398
+ def docs(self) -> str:
399
+ r"""Get the URL for the API documentation.
400
+
401
+ Returns:
402
+ str: The URL for the API documentation.
403
+ """
404
+ return f"http://localhost:{self.port}/docs"
@@ -0,0 +1,199 @@
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 json
15
+ import logging
16
+ from functools import wraps
17
+ from typing import List, Optional, Union
18
+
19
+ from camel.agents import ChatAgent
20
+ from camel.configs import ChatGPTConfig
21
+ from camel.models import BaseModelBackend, ModelFactory
22
+ from camel.runtime import BaseRuntime
23
+ from camel.runtime.utils import FunctionRiskToolkit, IgnoreRiskToolkit
24
+ from camel.toolkits import FunctionTool
25
+ from camel.types import ModelPlatformType, ModelType
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ GUARDPROMPT = """You are a function safety evaluator tasked with assessing the
30
+ potential risk level of a given function based on both its description
31
+ and parameters. Your goal is to determine if the function may pose any
32
+ harm to the user's environment, such as deleting files, executing
33
+ arbitrary code, or accessing sensitive information. Pay special attention
34
+ to the provided parameters — even if a function has the potential to be
35
+ dangerous, its actual parameters may indicate harmless behavior, and the
36
+ risk level should be adjusted accordingly. Use the `function_risk` tool to
37
+ assign a risk score to the function, based on the following criteria:
38
+
39
+ - **Score 1**: No harm. This includes simple operations like mathematical
40
+ calculations, content searches, or data retrievals that do not impact
41
+ the user's environment or access sensitive resources. This also
42
+ includes functions with potentially dangerous capabilities that have
43
+ harmless, controlled parameters that ensure safe execution.
44
+ - **Score 2**: Minimal harm. The function might read user files, retrieve
45
+ non-sensitive data, or access other low-risk resources,
46
+ posing little risk to the user.
47
+ - **Score 3**: Risk present. The function might delete files, modify the
48
+ file system, execute arbitrary code, or access sensitive data, which
49
+ could negatively impact the user's environment. However, if the
50
+ actual parameters provided clearly indicate safe and restricted
51
+ usage, this risk should be downgraded accordingly.
52
+
53
+ When evaluating, always consider both the function's description and its
54
+ specific parameters. If the function appears risky due to its design but
55
+ the provided parameters indicate a safe and non-impactful operation,
56
+ adjust the risk score to reflect this. Assign an appropriate risk score
57
+ and provide a brief explanation of your reasoning based on the function's
58
+ description and the actual parameters given.
59
+ YOU MUST USE THE `function_risk` TOOL TO ASSESS THE RISK
60
+ LEVEL OF EACH FUNCTION.
61
+ """
62
+
63
+
64
+ class LLMGuardRuntime(BaseRuntime):
65
+ r"""A runtime that evaluates the risk level of functions using
66
+ a language model.
67
+
68
+ Arguments:
69
+ prompt (str): The prompt to use for the language model. (default:
70
+ :obj:`GUARDPROMPT`)
71
+ model (BaseModelBackend): The language model to use. (default::obj:
72
+ `None`)
73
+ verbose (bool): Whether to print verbose output. (default::obj:
74
+ `False`)
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ prompt: str = GUARDPROMPT,
80
+ model: Optional[BaseModelBackend] = None,
81
+ verbose: bool = False,
82
+ ):
83
+ super().__init__()
84
+ self.prompt = prompt
85
+ self.model = model
86
+ self.verbose = verbose
87
+
88
+ if not self.model:
89
+ self.model = ModelFactory.create(
90
+ model_platform=ModelPlatformType.DEFAULT,
91
+ model_type=ModelType.DEFAULT,
92
+ model_config_dict=ChatGPTConfig().as_dict(),
93
+ )
94
+ self.ignore_toolkit = IgnoreRiskToolkit(verbose=verbose)
95
+ self.ignore_tool = self.ignore_toolkit.get_tools()[0]
96
+ self.tools_map[self.ignore_tool.get_function_name()] = self.ignore_tool
97
+
98
+ self.agent = ChatAgent(
99
+ system_message=self.prompt,
100
+ model=self.model,
101
+ external_tools=[
102
+ *FunctionRiskToolkit(verbose=verbose).get_tools(),
103
+ ],
104
+ )
105
+
106
+ def add( # type: ignore[override]
107
+ self,
108
+ funcs: Union[FunctionTool, List[FunctionTool]],
109
+ threshold: int = 2,
110
+ ) -> "LLMGuardRuntime":
111
+ r"""Add a function or list of functions to the runtime.
112
+
113
+ Args:
114
+ funcs (FunctionTool or List[FunctionTool]): The function or
115
+ list of functions to add.
116
+ threshold (int): The risk threshold for functions.
117
+ (default::obj:`2`)
118
+
119
+ Returns:
120
+ LLMGuardRuntime: The current runtime.
121
+ """
122
+
123
+ if not isinstance(funcs, list):
124
+ funcs = [funcs]
125
+
126
+ for func in funcs:
127
+ inner_func = func.func
128
+
129
+ # Create a wrapper that explicitly binds `func`
130
+ @wraps(inner_func)
131
+ def wrapper(
132
+ *args,
133
+ func=func,
134
+ inner_func=inner_func,
135
+ threshold=threshold,
136
+ **kwargs,
137
+ ):
138
+ function_name = func.get_function_name()
139
+ if function_name in self.ignore_toolkit.ignored_risks:
140
+ reason = self.ignore_toolkit.ignored_risks.pop(
141
+ function_name
142
+ )
143
+ logger.info(
144
+ f"Ignored risk for function {function_name}: {reason}"
145
+ )
146
+ return inner_func(*args, **kwargs)
147
+ self.agent.init_messages()
148
+ resp = self.agent.step(
149
+ f"""
150
+ Function is: {function_name}
151
+ Function description: {func.get_function_description()}
152
+ Args: {args}
153
+ Kwargs: {kwargs}
154
+ """
155
+ )
156
+ tool_call = resp.info.get("external_tool_request", None)
157
+ if not tool_call:
158
+ logger.error("No tool call found in response.")
159
+ return {
160
+ "error": "Risk assessment failed. Disabling function."
161
+ }
162
+ data = tool_call.function.arguments
163
+ data = json.loads(data)
164
+ if threshold < data["score"]:
165
+ message = (
166
+ f"Risk assessment not passed for {function_name}."
167
+ f"Score: {data['score']} > Threshold: {threshold}"
168
+ f"\nReason: {data['reason']}"
169
+ )
170
+ logger.warning(message)
171
+ return {"error": message}
172
+
173
+ logger.info(
174
+ (
175
+ f"Function {function_name} passed risk assessment."
176
+ f"Score: {data['score']}, Reason: {data['reason']}"
177
+ )
178
+ )
179
+ if self.verbose:
180
+ print(
181
+ (
182
+ f"Function {function_name} passed risk assessment."
183
+ f"Score: {data['score']}, Reason: {data['reason']}"
184
+ )
185
+ )
186
+ return inner_func(*args, **kwargs)
187
+
188
+ func.func = wrapper
189
+ self.tools_map[func.get_function_name()] = func
190
+ self.ignore_toolkit.add(func.get_function_name())
191
+
192
+ return self
193
+
194
+ def reset(self) -> "LLMGuardRuntime":
195
+ r"""Resets the runtime to its initial state."""
196
+ self.ignore_toolkit.ignored_risks = dict()
197
+ self.agent.reset()
198
+
199
+ return self