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.
- camel/__init__.py +10 -5
- camel/agents/__init__.py +4 -4
- camel/agents/base.py +4 -4
- camel/agents/chat_agent.py +106 -42
- camel/agents/critic_agent.py +4 -4
- camel/agents/deductive_reasoner_agent.py +8 -5
- camel/agents/embodied_agent.py +4 -4
- camel/agents/knowledge_graph_agent.py +4 -4
- camel/agents/role_assignment_agent.py +4 -4
- camel/agents/search_agent.py +4 -4
- camel/agents/task_agent.py +4 -4
- camel/agents/tool_agents/__init__.py +4 -4
- camel/agents/tool_agents/base.py +4 -4
- camel/agents/tool_agents/hugging_face_tool_agent.py +4 -4
- camel/bots/__init__.py +4 -4
- camel/bots/discord_app.py +4 -4
- camel/bots/slack/__init__.py +4 -4
- camel/bots/slack/models.py +4 -4
- camel/bots/slack/slack_app.py +4 -4
- camel/bots/telegram_bot.py +4 -4
- camel/configs/__init__.py +13 -4
- camel/configs/anthropic_config.py +4 -4
- camel/configs/base_config.py +4 -4
- camel/configs/cohere_config.py +76 -0
- camel/configs/deepseek_config.py +134 -0
- camel/configs/gemini_config.py +85 -127
- camel/configs/groq_config.py +4 -4
- camel/configs/litellm_config.py +4 -4
- camel/configs/mistral_config.py +4 -7
- camel/configs/nvidia_config.py +70 -0
- camel/configs/ollama_config.py +4 -4
- camel/configs/openai_config.py +32 -7
- camel/configs/qwen_config.py +4 -4
- camel/configs/reka_config.py +4 -4
- camel/configs/samba_config.py +4 -4
- camel/configs/togetherai_config.py +4 -4
- camel/configs/vllm_config.py +14 -5
- camel/configs/yi_config.py +4 -4
- camel/configs/zhipuai_config.py +4 -4
- camel/embeddings/__init__.py +6 -4
- camel/embeddings/base.py +4 -4
- camel/embeddings/mistral_embedding.py +4 -4
- camel/embeddings/openai_compatible_embedding.py +91 -0
- camel/embeddings/openai_embedding.py +4 -4
- camel/embeddings/sentence_transformers_embeddings.py +4 -4
- camel/embeddings/vlm_embedding.py +8 -5
- camel/generators.py +4 -4
- camel/human.py +4 -4
- camel/interpreters/__init__.py +4 -4
- camel/interpreters/base.py +4 -4
- camel/interpreters/docker_interpreter.py +11 -6
- camel/interpreters/internal_python_interpreter.py +4 -4
- camel/interpreters/interpreter_error.py +4 -4
- camel/interpreters/ipython_interpreter.py +4 -4
- camel/interpreters/subprocess_interpreter.py +11 -6
- camel/loaders/__init__.py +4 -4
- camel/loaders/apify_reader.py +4 -4
- camel/loaders/base_io.py +4 -4
- camel/loaders/chunkr_reader.py +4 -4
- camel/loaders/firecrawl_reader.py +4 -7
- camel/loaders/jina_url_reader.py +4 -4
- camel/loaders/unstructured_io.py +4 -4
- camel/logger.py +112 -0
- camel/memories/__init__.py +4 -4
- camel/memories/agent_memories.py +4 -4
- camel/memories/base.py +4 -4
- camel/memories/blocks/__init__.py +4 -4
- camel/memories/blocks/chat_history_block.py +4 -4
- camel/memories/blocks/vectordb_block.py +4 -4
- camel/memories/context_creators/__init__.py +4 -4
- camel/memories/context_creators/score_based.py +4 -4
- camel/memories/records.py +4 -4
- camel/messages/__init__.py +20 -4
- camel/messages/base.py +118 -11
- camel/messages/conversion/__init__.py +31 -0
- camel/messages/conversion/alpaca.py +122 -0
- camel/messages/conversion/conversation_models.py +178 -0
- camel/messages/conversion/sharegpt/__init__.py +20 -0
- camel/messages/conversion/sharegpt/function_call_formatter.py +49 -0
- camel/messages/conversion/sharegpt/hermes/__init__.py +19 -0
- camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +128 -0
- camel/messages/func_message.py +50 -4
- camel/models/__init__.py +13 -4
- camel/models/anthropic_model.py +4 -4
- camel/models/azure_openai_model.py +4 -4
- camel/models/base_model.py +4 -4
- camel/models/cohere_model.py +282 -0
- camel/models/deepseek_model.py +139 -0
- camel/models/gemini_model.py +61 -146
- camel/models/groq_model.py +4 -4
- camel/models/litellm_model.py +4 -4
- camel/models/mistral_model.py +4 -4
- camel/models/model_factory.py +13 -4
- camel/models/model_manager.py +212 -0
- camel/models/nemotron_model.py +4 -4
- camel/models/nvidia_model.py +141 -0
- camel/models/ollama_model.py +4 -4
- camel/models/openai_audio_models.py +4 -4
- camel/models/openai_compatible_model.py +4 -4
- camel/models/openai_model.py +43 -4
- camel/models/qwen_model.py +4 -4
- camel/models/reka_model.py +4 -4
- camel/models/samba_model.py +6 -5
- camel/models/stub_model.py +4 -4
- camel/models/togetherai_model.py +4 -4
- camel/models/vllm_model.py +4 -4
- camel/models/yi_model.py +4 -4
- camel/models/zhipuai_model.py +4 -4
- camel/personas/__init__.py +17 -0
- camel/personas/persona.py +103 -0
- camel/personas/persona_hub.py +293 -0
- camel/prompts/__init__.py +6 -4
- camel/prompts/ai_society.py +4 -4
- camel/prompts/base.py +4 -4
- camel/prompts/code.py +4 -4
- camel/prompts/evaluation.py +4 -4
- camel/prompts/generate_text_embedding_data.py +4 -4
- camel/prompts/image_craft.py +4 -4
- camel/prompts/misalignment.py +4 -4
- camel/prompts/multi_condition_image_craft.py +4 -4
- camel/prompts/object_recognition.py +4 -4
- camel/prompts/persona_hub.py +61 -0
- camel/prompts/prompt_templates.py +4 -4
- camel/prompts/role_description_prompt_template.py +4 -4
- camel/prompts/solution_extraction.py +4 -4
- camel/prompts/task_prompt_template.py +4 -4
- camel/prompts/translation.py +4 -4
- camel/prompts/video_description_prompt.py +4 -4
- camel/responses/__init__.py +4 -4
- camel/responses/agent_responses.py +4 -4
- camel/retrievers/__init__.py +4 -4
- camel/retrievers/auto_retriever.py +4 -4
- camel/retrievers/base.py +4 -4
- camel/retrievers/bm25_retriever.py +4 -4
- camel/retrievers/cohere_rerank_retriever.py +7 -9
- camel/retrievers/vector_retriever.py +26 -9
- camel/runtime/__init__.py +29 -0
- camel/runtime/api.py +93 -0
- camel/runtime/base.py +45 -0
- camel/runtime/configs.py +56 -0
- camel/runtime/docker_runtime.py +404 -0
- camel/runtime/llm_guard_runtime.py +199 -0
- camel/runtime/remote_http_runtime.py +204 -0
- camel/runtime/utils/__init__.py +20 -0
- camel/runtime/utils/function_risk_toolkit.py +58 -0
- camel/runtime/utils/ignore_risk_toolkit.py +72 -0
- camel/schemas/__init__.py +17 -0
- camel/schemas/base.py +45 -0
- camel/schemas/openai_converter.py +116 -0
- camel/societies/__init__.py +4 -4
- camel/societies/babyagi_playing.py +8 -5
- camel/societies/role_playing.py +4 -4
- camel/societies/workforce/__init__.py +4 -4
- camel/societies/workforce/base.py +4 -4
- camel/societies/workforce/prompts.py +4 -4
- camel/societies/workforce/role_playing_worker.py +4 -4
- camel/societies/workforce/single_agent_worker.py +4 -4
- camel/societies/workforce/task_channel.py +4 -4
- camel/societies/workforce/utils.py +4 -4
- camel/societies/workforce/worker.py +4 -4
- camel/societies/workforce/workforce.py +7 -7
- camel/storages/__init__.py +4 -4
- camel/storages/graph_storages/__init__.py +4 -4
- camel/storages/graph_storages/base.py +4 -4
- camel/storages/graph_storages/graph_element.py +4 -4
- camel/storages/graph_storages/nebula_graph.py +4 -4
- camel/storages/graph_storages/neo4j_graph.py +4 -4
- camel/storages/key_value_storages/__init__.py +4 -4
- camel/storages/key_value_storages/base.py +4 -4
- camel/storages/key_value_storages/in_memory.py +4 -4
- camel/storages/key_value_storages/json.py +4 -4
- camel/storages/key_value_storages/redis.py +4 -4
- camel/storages/object_storages/__init__.py +4 -4
- camel/storages/object_storages/amazon_s3.py +4 -4
- camel/storages/object_storages/azure_blob.py +4 -4
- camel/storages/object_storages/base.py +4 -4
- camel/storages/object_storages/google_cloud.py +4 -4
- camel/storages/vectordb_storages/__init__.py +4 -4
- camel/storages/vectordb_storages/base.py +4 -4
- camel/storages/vectordb_storages/milvus.py +4 -4
- camel/storages/vectordb_storages/qdrant.py +4 -4
- camel/tasks/__init__.py +4 -4
- camel/tasks/task.py +4 -4
- camel/tasks/task_prompt.py +4 -4
- camel/terminators/__init__.py +4 -4
- camel/terminators/base.py +4 -4
- camel/terminators/response_terminator.py +4 -4
- camel/terminators/token_limit_terminator.py +4 -4
- camel/toolkits/__init__.py +16 -17
- camel/toolkits/arxiv_toolkit.py +4 -4
- camel/toolkits/ask_news_toolkit.py +7 -18
- camel/toolkits/base.py +4 -4
- camel/toolkits/code_execution.py +57 -10
- camel/toolkits/dalle_toolkit.py +4 -7
- camel/toolkits/data_commons_toolkit.py +4 -4
- camel/toolkits/function_tool.py +220 -69
- camel/toolkits/github_toolkit.py +4 -4
- camel/toolkits/google_maps_toolkit.py +4 -4
- camel/toolkits/google_scholar_toolkit.py +4 -4
- camel/toolkits/human_toolkit.py +53 -0
- camel/toolkits/linkedin_toolkit.py +4 -4
- camel/toolkits/math_toolkit.py +4 -7
- camel/toolkits/meshy_toolkit.py +185 -0
- camel/toolkits/notion_toolkit.py +4 -4
- camel/toolkits/open_api_specs/biztoc/__init__.py +4 -4
- camel/toolkits/open_api_specs/coursera/__init__.py +4 -4
- camel/toolkits/open_api_specs/create_qr_code/__init__.py +4 -4
- camel/toolkits/open_api_specs/klarna/__init__.py +4 -4
- camel/toolkits/open_api_specs/nasa_apod/__init__.py +4 -4
- camel/toolkits/open_api_specs/outschool/__init__.py +4 -4
- camel/toolkits/open_api_specs/outschool/paths/__init__.py +4 -4
- camel/toolkits/open_api_specs/outschool/paths/get_classes.py +4 -4
- camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +4 -4
- camel/toolkits/open_api_specs/security_config.py +4 -4
- camel/toolkits/open_api_specs/speak/__init__.py +4 -4
- camel/toolkits/open_api_specs/web_scraper/__init__.py +4 -4
- camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +4 -4
- camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +4 -4
- camel/toolkits/open_api_toolkit.py +4 -4
- camel/toolkits/reddit_toolkit.py +4 -4
- camel/toolkits/retrieval_toolkit.py +4 -4
- camel/toolkits/search_toolkit.py +49 -29
- camel/toolkits/slack_toolkit.py +4 -4
- camel/toolkits/twitter_toolkit.py +13 -13
- camel/toolkits/video_toolkit.py +211 -0
- camel/toolkits/weather_toolkit.py +4 -7
- camel/toolkits/whatsapp_toolkit.py +6 -6
- camel/types/__init__.py +6 -4
- camel/types/enums.py +118 -15
- camel/types/openai_types.py +6 -4
- camel/types/unified_model_type.py +9 -4
- camel/utils/__init__.py +35 -33
- camel/utils/async_func.py +4 -4
- camel/utils/commons.py +26 -9
- camel/utils/constants.py +4 -4
- camel/utils/response_format.py +63 -0
- camel/utils/token_counting.py +8 -5
- {camel_ai-0.2.9.dist-info → camel_ai-0.2.11.dist-info}/METADATA +108 -56
- camel_ai-0.2.11.dist-info/RECORD +252 -0
- camel_ai-0.2.9.dist-info/RECORD +0 -215
- {camel_ai-0.2.9.dist-info → camel_ai-0.2.11.dist-info}/LICENSE +0 -0
- {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
|