Spaces:
Configuration error
Configuration error
Upload 302 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .container/.dockerignore +74 -0
- .container/DOCKER_README.md +298 -0
- .container/Dockerfile +106 -0
- .container/build_docker.bat +147 -0
- .container/build_docker.sh +150 -0
- .container/check_docker.bat +62 -0
- .container/check_docker.sh +92 -0
- .container/docker-compose.yml +52 -0
- .container/run_in_docker.bat +116 -0
- .container/run_in_docker.sh +135 -0
- .gitattributes +8 -0
- .gitignore +60 -0
- DOCKER_README_en.md +298 -0
- README.md +311 -0
- README_zh.md +296 -0
- assets/community.png +3 -0
- assets/community_2.png +3 -0
- assets/community_3.jpg +3 -0
- assets/community_4.jpg +3 -0
- assets/community_5.jpg +3 -0
- assets/meetup.jpg +3 -0
- assets/owl_architecture.png +3 -0
- assets/qr_code.jpg +3 -0
- licenses/LICENSE +13 -0
- licenses/license_template.txt +13 -0
- licenses/update_license.py +132 -0
- owl/.env_template +28 -0
- owl/camel/__init__.py +25 -0
- owl/camel/agents/__init__.py +44 -0
- owl/camel/agents/base.py +29 -0
- owl/camel/agents/chat_agent.py +1411 -0
- owl/camel/agents/critic_agent.py +202 -0
- owl/camel/agents/deductive_reasoner_agent.py +303 -0
- owl/camel/agents/embodied_agent.py +201 -0
- owl/camel/agents/knowledge_graph_agent.py +259 -0
- owl/camel/agents/role_assignment_agent.py +141 -0
- owl/camel/agents/search_agent.py +133 -0
- owl/camel/agents/task_agent.py +410 -0
- owl/camel/agents/tool_agents/__init__.py +20 -0
- owl/camel/agents/tool_agents/base.py +39 -0
- owl/camel/agents/tool_agents/hugging_face_tool_agent.py +206 -0
- owl/camel/benchmarks/__init__.py +17 -0
- owl/camel/benchmarks/base.py +152 -0
- owl/camel/bots/__init__.py +34 -0
- owl/camel/bots/discord_app.py +138 -0
- owl/camel/bots/slack/__init__.py +30 -0
- owl/camel/bots/slack/models.py +158 -0
- owl/camel/bots/slack/slack_app.py +255 -0
- owl/camel/bots/telegram_bot.py +82 -0
- owl/camel/configs/__init__.py +76 -0
.container/.dockerignore
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Git
|
2 |
+
.git
|
3 |
+
.gitignore
|
4 |
+
.github
|
5 |
+
|
6 |
+
# Docker
|
7 |
+
Dockerfile
|
8 |
+
docker-compose.yml
|
9 |
+
.dockerignore
|
10 |
+
DOCKER_README.md
|
11 |
+
run_in_docker.sh
|
12 |
+
|
13 |
+
# Python
|
14 |
+
__pycache__/
|
15 |
+
*.py[cod]
|
16 |
+
*$py.class
|
17 |
+
*.so
|
18 |
+
.Python
|
19 |
+
env/
|
20 |
+
build/
|
21 |
+
develop-eggs/
|
22 |
+
dist/
|
23 |
+
downloads/
|
24 |
+
eggs/
|
25 |
+
.eggs/
|
26 |
+
lib/
|
27 |
+
lib64/
|
28 |
+
parts/
|
29 |
+
sdist/
|
30 |
+
var/
|
31 |
+
*.egg-info/
|
32 |
+
.installed.cfg
|
33 |
+
*.egg
|
34 |
+
.pytest_cache/
|
35 |
+
.coverage
|
36 |
+
htmlcov/
|
37 |
+
|
38 |
+
# 虚拟环境
|
39 |
+
venv/
|
40 |
+
ENV/
|
41 |
+
env/
|
42 |
+
.env
|
43 |
+
|
44 |
+
# IDE
|
45 |
+
.idea/
|
46 |
+
.vscode/
|
47 |
+
*.swp
|
48 |
+
*.swo
|
49 |
+
.DS_Store
|
50 |
+
|
51 |
+
# 临时文件
|
52 |
+
temp_*
|
53 |
+
*.tmp
|
54 |
+
*.log
|
55 |
+
*.bak
|
56 |
+
|
57 |
+
# 缓存
|
58 |
+
.cache/
|
59 |
+
.npm/
|
60 |
+
.yarn/
|
61 |
+
|
62 |
+
# 大型数据文件
|
63 |
+
*.csv
|
64 |
+
*.sqlite
|
65 |
+
*.db
|
66 |
+
*.hdf5
|
67 |
+
*.h5
|
68 |
+
*.parquet
|
69 |
+
*.feather
|
70 |
+
*.pkl
|
71 |
+
*.pickle
|
72 |
+
|
73 |
+
# 数据目录
|
74 |
+
data/
|
.container/DOCKER_README.md
ADDED
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# OWL项目Docker使用指南
|
2 |
+
|
3 |
+
本文档提供了如何使用Docker运行OWL项目的详细说明。
|
4 |
+
|
5 |
+
## 前提条件
|
6 |
+
|
7 |
+
- 安装 [Docker](https://docs.docker.com/get-docker/)
|
8 |
+
- 安装 [Docker Compose](https://docs.docker.com/compose/install/) (推荐v2.x版本)
|
9 |
+
- 获取必要的API密钥(OpenAI API等)
|
10 |
+
|
11 |
+
## 技术说明
|
12 |
+
|
13 |
+
本Docker配置使用了以下技术来确保OWL项目在容器中正常运行:
|
14 |
+
|
15 |
+
- **Xvfb**:虚拟帧缓冲区,用于在无显示器的环境中模拟X服务器
|
16 |
+
- **Playwright**:用于自动化浏览器操作,配置为无头模式
|
17 |
+
- **共享内存**:增加了共享内存大小,以提高浏览器性能
|
18 |
+
- **BuildKit**:使用Docker BuildKit加速构建过程
|
19 |
+
- **缓存优化**:使用持久化卷缓存pip和Playwright依赖
|
20 |
+
- **跨平台兼容**:提供了适用于Windows和macOS/Linux的脚本
|
21 |
+
|
22 |
+
## Docker Compose版本说明
|
23 |
+
|
24 |
+
本项目使用的docker-compose.yml文件兼容Docker Compose v2.x版本。如果您使用的是较旧的Docker Compose v1.x版本,可能需要手动添加版本号:
|
25 |
+
|
26 |
+
```yaml
|
27 |
+
version: '3'
|
28 |
+
|
29 |
+
services:
|
30 |
+
# ...其余配置保持不变
|
31 |
+
```
|
32 |
+
|
33 |
+
## 快速开始
|
34 |
+
|
35 |
+
### 0. 检查环境
|
36 |
+
|
37 |
+
首先,运行检查脚本确保您的环境已准备好:
|
38 |
+
|
39 |
+
#### 在macOS/Linux上检查
|
40 |
+
|
41 |
+
```bash
|
42 |
+
# 先给脚本添加执行权限
|
43 |
+
chmod +x check_docker.sh
|
44 |
+
|
45 |
+
# 运行检查脚本
|
46 |
+
./check_docker.sh
|
47 |
+
```
|
48 |
+
|
49 |
+
#### 在Windows上检查
|
50 |
+
|
51 |
+
```cmd
|
52 |
+
check_docker.bat
|
53 |
+
```
|
54 |
+
|
55 |
+
如果检查脚本发现任何问题,请按照提示进行修复。
|
56 |
+
|
57 |
+
### 1. 配置环境变量
|
58 |
+
|
59 |
+
复制环境变量模板文件并填写必要的API密钥:
|
60 |
+
|
61 |
+
```bash
|
62 |
+
cp owl/.env_template owl/.env
|
63 |
+
```
|
64 |
+
|
65 |
+
然后编辑 `owl/.env` 文件,填写必要的API密钥,例如:
|
66 |
+
|
67 |
+
```
|
68 |
+
OPENAI_API_KEY=your_openai_api_key
|
69 |
+
GOOGLE_API_KEY=your_google_api_key
|
70 |
+
SEARCH_ENGINE_ID=your_search_engine_id
|
71 |
+
```
|
72 |
+
|
73 |
+
### 2. 快速构建Docker镜像
|
74 |
+
|
75 |
+
#### 在macOS/Linux上构建
|
76 |
+
|
77 |
+
使用提供的Shell脚本,可以加速Docker镜像的构建:
|
78 |
+
|
79 |
+
```bash
|
80 |
+
# 先给脚本添加执行权限
|
81 |
+
chmod +x build_docker.sh
|
82 |
+
|
83 |
+
# 运行构建脚本
|
84 |
+
./build_docker.sh
|
85 |
+
```
|
86 |
+
|
87 |
+
#### 在Windows上构建
|
88 |
+
|
89 |
+
使用提供的批处理文件:
|
90 |
+
|
91 |
+
```cmd
|
92 |
+
build_docker.bat
|
93 |
+
```
|
94 |
+
|
95 |
+
或者使用标准方式构建并启动:
|
96 |
+
|
97 |
+
```bash
|
98 |
+
# 使用BuildKit加速构建
|
99 |
+
set DOCKER_BUILDKIT=1
|
100 |
+
set COMPOSE_DOCKER_CLI_BUILD=1
|
101 |
+
docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1
|
102 |
+
|
103 |
+
# 启动容器
|
104 |
+
docker-compose up -d
|
105 |
+
```
|
106 |
+
|
107 |
+
### 3. 交互式使用容器
|
108 |
+
|
109 |
+
容器启动后,会自动进入交互式shell环境,并显示欢迎信息和可用脚本列表:
|
110 |
+
|
111 |
+
```bash
|
112 |
+
# 进入容器(如果没有自动进入)
|
113 |
+
docker-compose exec owl bash
|
114 |
+
```
|
115 |
+
|
116 |
+
在容器内,您可以直接运行任何可用的脚本:
|
117 |
+
|
118 |
+
```bash
|
119 |
+
# 运行默认脚本
|
120 |
+
xvfb-python run.py
|
121 |
+
|
122 |
+
# 运行DeepSeek示例
|
123 |
+
xvfb-python run_deepseek_example.py
|
124 |
+
|
125 |
+
# 运行脚本并传递查询参数
|
126 |
+
xvfb-python run.py "什么是人工智能?"
|
127 |
+
```
|
128 |
+
|
129 |
+
### 4. 使用外部脚本运行查询
|
130 |
+
|
131 |
+
#### 在macOS/Linux上运行
|
132 |
+
|
133 |
+
```bash
|
134 |
+
# 先给脚本添加执行权限
|
135 |
+
chmod +x run_in_docker.sh
|
136 |
+
|
137 |
+
# 默认使用 run.py 脚本
|
138 |
+
./run_in_docker.sh "你的问题"
|
139 |
+
|
140 |
+
# 指定使用特定脚本
|
141 |
+
./run_in_docker.sh run_deepseek_example.py "你的问题"
|
142 |
+
```
|
143 |
+
|
144 |
+
#### 在Windows上运行
|
145 |
+
|
146 |
+
```cmd
|
147 |
+
REM 默认使用 run.py 脚本
|
148 |
+
run_in_docker.bat "你的问题"
|
149 |
+
|
150 |
+
REM 指定使用特定脚本
|
151 |
+
run_in_docker.bat run_deepseek_example.py "你的问题"
|
152 |
+
```
|
153 |
+
|
154 |
+
**可用脚本**:
|
155 |
+
- `run.py` - 默认脚本,使用OpenAI GPT-4o模型
|
156 |
+
- `run_deepseek_example.py` - 使用DeepSeek模型
|
157 |
+
- `run_gaia_roleplaying.py` - GAIA基准测试脚本
|
158 |
+
|
159 |
+
## 目录挂载
|
160 |
+
|
161 |
+
Docker Compose配置中已经设置了以下挂载点:
|
162 |
+
|
163 |
+
- `./owl/.env:/app/owl/.env`:挂载环境变量文件,方便修改API密钥
|
164 |
+
- `./data:/app/data`:挂载数据目录,用于存储和访问数据文件
|
165 |
+
- `playwright-cache`:持久化卷,用于缓存Playwright浏览器
|
166 |
+
- `pip-cache`:持久化卷,用于缓存pip包
|
167 |
+
|
168 |
+
## 环境变量
|
169 |
+
|
170 |
+
您可以通过以下两种方式设置环境变量:
|
171 |
+
|
172 |
+
1. 修改 `owl/.env` 文件
|
173 |
+
2. 在 `docker-compose.yml` 文件的 `environment` 部分添加环境变量
|
174 |
+
|
175 |
+
## 构建优化
|
176 |
+
|
177 |
+
本Docker配置包含多项构建优化:
|
178 |
+
|
179 |
+
1. **使用国内镜像源**:使用清华大学镜像源加速pip包下载
|
180 |
+
2. **层优化**:减少Dockerfile中的层数,提高构建效率
|
181 |
+
3. **缓存利用**:
|
182 |
+
- 启用pip缓存,避免重复下载依赖包
|
183 |
+
- 使用Docker BuildKit内联缓存
|
184 |
+
- 合理安排Dockerfile指令顺序,最大化利用缓存
|
185 |
+
4. **BuildKit**:启用Docker BuildKit加速构建
|
186 |
+
5. **持久化缓存**:
|
187 |
+
- 使用Docker卷缓存pip包(`pip-cache`)
|
188 |
+
- 使用Docker卷缓存Playwright浏览器(`playwright-cache`)
|
189 |
+
- 本地缓存目录(`.docker-cache`)
|
190 |
+
|
191 |
+
### 缓存清理
|
192 |
+
|
193 |
+
如果需要清理缓存,可以使用以下命令:
|
194 |
+
|
195 |
+
```bash
|
196 |
+
# 清理Docker构建缓存
|
197 |
+
docker builder prune
|
198 |
+
|
199 |
+
# 清理Docker卷(会删除所有未使用的卷,包括缓存卷)
|
200 |
+
docker volume prune
|
201 |
+
|
202 |
+
# 清理本��缓存目录
|
203 |
+
rm -rf .docker-cache
|
204 |
+
```
|
205 |
+
|
206 |
+
## 跨平台兼容性
|
207 |
+
|
208 |
+
本项目提供了适用于不同操作系统的脚本:
|
209 |
+
|
210 |
+
1. **检查脚本**:
|
211 |
+
- `check_docker.sh`(macOS/Linux):检查Docker环境
|
212 |
+
- `check_docker.bat`(Windows):检查Docker环境
|
213 |
+
|
214 |
+
2. **构建脚本**:
|
215 |
+
- `build_docker.sh`(macOS/Linux):构建Docker镜像
|
216 |
+
- `build_docker.bat`(Windows):构建Docker镜像
|
217 |
+
|
218 |
+
3. **运行脚本**:
|
219 |
+
- `run_in_docker.sh`(macOS/Linux):运行Docker容器中的脚本
|
220 |
+
- `run_in_docker.bat`(Windows):运行Docker容器中的脚本
|
221 |
+
|
222 |
+
这些脚本会自动检测操作系统类型,并使用适当的命令。
|
223 |
+
|
224 |
+
## 故障排除
|
225 |
+
|
226 |
+
### 容器无法启动
|
227 |
+
|
228 |
+
检查日志以获取更多信息:
|
229 |
+
|
230 |
+
```bash
|
231 |
+
docker-compose logs
|
232 |
+
```
|
233 |
+
|
234 |
+
### API密钥问题
|
235 |
+
|
236 |
+
确保您已经在 `owl/.env` 文件中正确设置了所有必要的API密钥。
|
237 |
+
|
238 |
+
### Docker Compose警告
|
239 |
+
|
240 |
+
如果您看到关于`version`属性过时的警告:
|
241 |
+
|
242 |
+
```
|
243 |
+
WARN[0000] docker-compose.yml: the attribute `version` is obsolete
|
244 |
+
```
|
245 |
+
|
246 |
+
这是因为您使用的是Docker Compose v2.x,它不再需要显式指定版本号。我们已经从配置文件中移除了这个属性,所以您不会再看到这个警告。
|
247 |
+
|
248 |
+
### 浏览器相关问题
|
249 |
+
|
250 |
+
如果遇到浏览器相关的问题,可以尝试以下解决方案:
|
251 |
+
|
252 |
+
1. 确保在Docker容器中使用`xvfb-python`命令运行Python脚本
|
253 |
+
2. 检查是否正确安装了Xvfb和相关依赖
|
254 |
+
3. 增加共享内存大小(在docker-compose.yml中已设置为2GB)
|
255 |
+
|
256 |
+
### 构建速度慢
|
257 |
+
|
258 |
+
如果构建速度慢,可以尝试以下解决方案:
|
259 |
+
|
260 |
+
1. 确保启用了Docker BuildKit(`DOCKER_BUILDKIT=1`)
|
261 |
+
2. 确保启用了pip缓存(已在docker-compose.yml中配置)
|
262 |
+
3. 使用`--build-arg BUILDKIT_INLINE_CACHE=1`参数构建(已在构建脚本中配置)
|
263 |
+
4. 如果是首次构建,下载依赖包可能需要较长时间,后续构建会更快
|
264 |
+
|
265 |
+
### Windows特有问题
|
266 |
+
|
267 |
+
如果在Windows上遇到问题:
|
268 |
+
|
269 |
+
1. 确保使用管理员权限运行命令提示符或PowerShell
|
270 |
+
2. 如果遇到路径问题,尝试使用正斜杠(/)而不是反斜杠(\)
|
271 |
+
3. 如果遇到Docker Compose命令问题,尝试使用`docker compose`(无连字符)
|
272 |
+
|
273 |
+
### 内存不足
|
274 |
+
|
275 |
+
如果遇到内存不足的问题,可以在 `docker-compose.yml` 文件中调整资源限制:
|
276 |
+
|
277 |
+
```yaml
|
278 |
+
services:
|
279 |
+
owl:
|
280 |
+
# 其他配置...
|
281 |
+
deploy:
|
282 |
+
resources:
|
283 |
+
limits:
|
284 |
+
cpus: '4' # 增加CPU核心数
|
285 |
+
memory: 8G # 增加内存限制
|
286 |
+
```
|
287 |
+
|
288 |
+
## 自定义Docker镜像
|
289 |
+
|
290 |
+
如果需要自定义Docker镜像,可以修改 `Dockerfile` 文件,然后重新构建:
|
291 |
+
|
292 |
+
```bash
|
293 |
+
# macOS/Linux
|
294 |
+
./build_docker.sh
|
295 |
+
|
296 |
+
# Windows
|
297 |
+
build_docker.bat
|
298 |
+
```
|
.container/Dockerfile
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 使用ARG定义可配置的构建参数
|
2 |
+
ARG PYTHON_VERSION=3.10
|
3 |
+
ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
4 |
+
ARG PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
|
5 |
+
|
6 |
+
# 第一阶段:构建依赖
|
7 |
+
FROM python:${PYTHON_VERSION}-slim AS builder
|
8 |
+
|
9 |
+
# 设置工作目录
|
10 |
+
WORKDIR /build
|
11 |
+
|
12 |
+
# 设置pip镜像源以加速下载
|
13 |
+
ARG PIP_INDEX_URL
|
14 |
+
RUN pip config set global.index-url ${PIP_INDEX_URL}
|
15 |
+
|
16 |
+
# 安装构建依赖
|
17 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
18 |
+
build-essential \
|
19 |
+
&& apt-get clean \
|
20 |
+
&& rm -rf /var/lib/apt/lists/*
|
21 |
+
|
22 |
+
# 复制并安装requirements.txt
|
23 |
+
COPY requirements.txt .
|
24 |
+
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
25 |
+
|
26 |
+
# 第二阶段:运行时环境
|
27 |
+
FROM python:${PYTHON_VERSION}-slim
|
28 |
+
|
29 |
+
# 添加构建信息标签
|
30 |
+
ARG BUILD_DATE
|
31 |
+
ARG VERSION
|
32 |
+
LABEL org.opencontainers.image.created="${BUILD_DATE}" \
|
33 |
+
org.opencontainers.image.version="${VERSION}" \
|
34 |
+
org.opencontainers.image.title="OWL Project" \
|
35 |
+
org.opencontainers.image.description="OWL Project Docker Image" \
|
36 |
+
org.opencontainers.image.source="https://github.com/yourusername/owl"
|
37 |
+
|
38 |
+
# 设置工作目录
|
39 |
+
WORKDIR /app
|
40 |
+
|
41 |
+
# 设置pip镜像源以加速下载
|
42 |
+
ARG PIP_INDEX_URL
|
43 |
+
RUN pip config set global.index-url ${PIP_INDEX_URL}
|
44 |
+
|
45 |
+
# 从builder阶段复制已安装的Python包
|
46 |
+
COPY --from=builder /install /usr/local
|
47 |
+
|
48 |
+
# 优化apt安装,减少层数
|
49 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
50 |
+
curl \
|
51 |
+
git \
|
52 |
+
ffmpeg \
|
53 |
+
libsm6 \
|
54 |
+
libxext6 \
|
55 |
+
# 添加xvfb和相关依赖
|
56 |
+
xvfb \
|
57 |
+
xauth \
|
58 |
+
x11-utils \
|
59 |
+
&& apt-get clean \
|
60 |
+
&& rm -rf /var/lib/apt/lists/*
|
61 |
+
|
62 |
+
# 安装 Playwright 依赖(使用国内镜像源)
|
63 |
+
ENV PLAYWRIGHT_BROWSERS_PATH=/root/.cache/ms-playwright
|
64 |
+
ARG PLAYWRIGHT_DOWNLOAD_HOST
|
65 |
+
ENV PLAYWRIGHT_DOWNLOAD_HOST=${PLAYWRIGHT_DOWNLOAD_HOST}
|
66 |
+
RUN pip install --no-cache-dir playwright && \
|
67 |
+
playwright install --with-deps chromium
|
68 |
+
|
69 |
+
# 创建非root用户
|
70 |
+
RUN groupadd -r owl && useradd -r -g owl -m owl
|
71 |
+
|
72 |
+
# 复制项目文件
|
73 |
+
COPY owl/ ./owl/
|
74 |
+
COPY licenses/ ./licenses/
|
75 |
+
COPY assets/ ./assets/
|
76 |
+
COPY README.md .
|
77 |
+
COPY README_zh.md .
|
78 |
+
|
79 |
+
# 设置环境变量文件
|
80 |
+
COPY owl/.env_template ./owl/.env
|
81 |
+
|
82 |
+
# 创建启动脚本
|
83 |
+
RUN echo '#!/bin/bash\nxvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" python "$@"' > /usr/local/bin/xvfb-python && \
|
84 |
+
chmod +x /usr/local/bin/xvfb-python
|
85 |
+
|
86 |
+
# 创建欢迎脚本
|
87 |
+
RUN echo '#!/bin/bash\necho "欢迎使用OWL项目Docker环境!"\necho ""\necho "可用的脚本:"\nls -1 *.py | grep -v "__" | sed "s/^/- /"\necho ""\necho "运行示例:"\necho " xvfb-python run.py # 运行默认脚本"\necho " xvfb-python run_deepseek_example.py # 运行DeepSeek示例"\necho ""\necho "或者使用自定义查询:"\necho " xvfb-python run.py \"你的问题\""\necho ""' > /usr/local/bin/owl-welcome && \
|
88 |
+
chmod +x /usr/local/bin/owl-welcome
|
89 |
+
|
90 |
+
# 设置工作目录
|
91 |
+
WORKDIR /app/owl
|
92 |
+
|
93 |
+
# 设置适当的权限
|
94 |
+
RUN chown -R owl:owl /app
|
95 |
+
RUN mkdir -p /root/.cache && chown -R owl:owl /root/.cache
|
96 |
+
|
97 |
+
# 切换到非root用户
|
98 |
+
# 注意:如果需要访问/dev/shm,可能仍需要root用户
|
99 |
+
# USER owl
|
100 |
+
|
101 |
+
# 添加健康检查
|
102 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
103 |
+
CMD python -c "import sys; sys.exit(0 if __import__('os').path.exists('/app/owl') else 1)"
|
104 |
+
|
105 |
+
# 容器启动命令
|
106 |
+
CMD ["/bin/bash", "-c", "owl-welcome && /bin/bash"]
|
.container/build_docker.bat
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
setlocal enabledelayedexpansion
|
3 |
+
|
4 |
+
echo 在Windows上构建Docker镜像...
|
5 |
+
|
6 |
+
REM 设置配置变量
|
7 |
+
set CACHE_DIR=.docker-cache\pip
|
8 |
+
set BUILD_ARGS=--build-arg BUILDKIT_INLINE_CACHE=1
|
9 |
+
set COMPOSE_FILE=docker-compose.yml
|
10 |
+
|
11 |
+
REM 解析命令行参数
|
12 |
+
set CLEAN_CACHE=0
|
13 |
+
set REBUILD=0
|
14 |
+
set SERVICE=
|
15 |
+
|
16 |
+
:parse_args
|
17 |
+
if "%~1"=="" goto :end_parse_args
|
18 |
+
if /i "%~1"=="--clean" (
|
19 |
+
set CLEAN_CACHE=1
|
20 |
+
shift
|
21 |
+
goto :parse_args
|
22 |
+
)
|
23 |
+
if /i "%~1"=="--rebuild" (
|
24 |
+
set REBUILD=1
|
25 |
+
shift
|
26 |
+
goto :parse_args
|
27 |
+
)
|
28 |
+
if /i "%~1"=="--service" (
|
29 |
+
set SERVICE=%~2
|
30 |
+
shift
|
31 |
+
shift
|
32 |
+
goto :parse_args
|
33 |
+
)
|
34 |
+
if /i "%~1"=="--help" (
|
35 |
+
echo 用法: build_docker.bat [选项]
|
36 |
+
echo 选项:
|
37 |
+
echo --clean 清理缓存目录
|
38 |
+
echo --rebuild 强制重新构建镜像
|
39 |
+
echo --service 指定要构建的服务名称
|
40 |
+
echo --help 显示此帮助信息
|
41 |
+
exit /b 0
|
42 |
+
)
|
43 |
+
shift
|
44 |
+
goto :parse_args
|
45 |
+
:end_parse_args
|
46 |
+
|
47 |
+
REM 检查Docker是否安装
|
48 |
+
where docker >nul 2>nul
|
49 |
+
if %ERRORLEVEL% NEQ 0 (
|
50 |
+
echo 错误: Docker未安装
|
51 |
+
echo 请先安装Docker Desktop: https://docs.docker.com/desktop/install/windows-install/
|
52 |
+
pause
|
53 |
+
exit /b 1
|
54 |
+
)
|
55 |
+
|
56 |
+
REM 检查Docker是否运行
|
57 |
+
docker info >nul 2>nul
|
58 |
+
if %ERRORLEVEL% NEQ 0 (
|
59 |
+
echo 错误: Docker未运行
|
60 |
+
echo 请启动Docker Desktop应用程序
|
61 |
+
pause
|
62 |
+
exit /b 1
|
63 |
+
)
|
64 |
+
|
65 |
+
REM 检查docker-compose.yml文件是否存在
|
66 |
+
if not exist "%COMPOSE_FILE%" (
|
67 |
+
echo 错误: 未找到%COMPOSE_FILE%文件
|
68 |
+
echo 请确保在正确的目录中运行此脚本
|
69 |
+
pause
|
70 |
+
exit /b 1
|
71 |
+
)
|
72 |
+
|
73 |
+
REM 检查Docker Compose命令
|
74 |
+
where docker-compose >nul 2>nul
|
75 |
+
if %ERRORLEVEL% EQU 0 (
|
76 |
+
set COMPOSE_CMD=docker-compose
|
77 |
+
) else (
|
78 |
+
echo 尝试使用新的docker compose命令...
|
79 |
+
docker compose version >nul 2>nul
|
80 |
+
if %ERRORLEVEL% EQU 0 (
|
81 |
+
set COMPOSE_CMD=docker compose
|
82 |
+
) else (
|
83 |
+
echo 错误: 未找到Docker Compose命令
|
84 |
+
echo 请确保Docker Desktop已正确安装
|
85 |
+
pause
|
86 |
+
exit /b 1
|
87 |
+
)
|
88 |
+
)
|
89 |
+
|
90 |
+
REM 设置Docker BuildKit环境变量
|
91 |
+
set DOCKER_BUILDKIT=1
|
92 |
+
set COMPOSE_DOCKER_CLI_BUILD=1
|
93 |
+
|
94 |
+
echo 启用Docker BuildKit加速构建...
|
95 |
+
|
96 |
+
REM 清理缓存(如果指定)
|
97 |
+
if %CLEAN_CACHE% EQU 1 (
|
98 |
+
echo 清理缓存目录...
|
99 |
+
if exist "%CACHE_DIR%" rmdir /s /q "%CACHE_DIR%"
|
100 |
+
)
|
101 |
+
|
102 |
+
REM 创建缓存目录
|
103 |
+
if not exist "%CACHE_DIR%" (
|
104 |
+
echo 创建缓存目录...
|
105 |
+
mkdir "%CACHE_DIR%"
|
106 |
+
)
|
107 |
+
|
108 |
+
REM 添加构建时间标记
|
109 |
+
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
|
110 |
+
set "YEAR=%dt:~0,4%"
|
111 |
+
set "MONTH=%dt:~4,2%"
|
112 |
+
set "DAY=%dt:~6,2%"
|
113 |
+
set "HOUR=%dt:~8,2%"
|
114 |
+
set "MINUTE=%dt:~10,2%"
|
115 |
+
set "BUILD_TIME=%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%"
|
116 |
+
set "BUILD_ARGS=%BUILD_ARGS% --build-arg BUILD_TIME=%BUILD_TIME%"
|
117 |
+
|
118 |
+
REM 构建Docker镜像
|
119 |
+
echo 开始构建Docker镜像...
|
120 |
+
|
121 |
+
if "%SERVICE%"=="" (
|
122 |
+
if %REBUILD% EQU 1 (
|
123 |
+
echo 强制重新构建所有服务...
|
124 |
+
%COMPOSE_CMD% build --no-cache %BUILD_ARGS%
|
125 |
+
) else (
|
126 |
+
%COMPOSE_CMD% build %BUILD_ARGS%
|
127 |
+
)
|
128 |
+
) else (
|
129 |
+
if %REBUILD% EQU 1 (
|
130 |
+
echo 强制重新构建服务 %SERVICE%...
|
131 |
+
%COMPOSE_CMD% build --no-cache %BUILD_ARGS% %SERVICE%
|
132 |
+
) else (
|
133 |
+
echo 构建服务 %SERVICE%...
|
134 |
+
%COMPOSE_CMD% build %BUILD_ARGS% %SERVICE%
|
135 |
+
)
|
136 |
+
)
|
137 |
+
|
138 |
+
if %ERRORLEVEL% EQU 0 (
|
139 |
+
echo Docker镜像构建成功!
|
140 |
+
echo 构建时间: %BUILD_TIME%
|
141 |
+
echo 可以使用以下命令启动容器:
|
142 |
+
echo %COMPOSE_CMD% up -d
|
143 |
+
) else (
|
144 |
+
echo Docker镜像构建失败,请检查错误信息。
|
145 |
+
)
|
146 |
+
|
147 |
+
pause
|
.container/build_docker.sh
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# 设置配置变量
|
4 |
+
CACHE_DIR=".docker-cache/pip"
|
5 |
+
BUILD_ARGS="--build-arg BUILDKIT_INLINE_CACHE=1"
|
6 |
+
COMPOSE_FILE="docker-compose.yml"
|
7 |
+
CLEAN_CACHE=0
|
8 |
+
REBUILD=0
|
9 |
+
SERVICE=""
|
10 |
+
|
11 |
+
# 解析命令行参数
|
12 |
+
while [[ $# -gt 0 ]]; do
|
13 |
+
case "$1" in
|
14 |
+
--clean)
|
15 |
+
CLEAN_CACHE=1
|
16 |
+
shift
|
17 |
+
;;
|
18 |
+
--rebuild)
|
19 |
+
REBUILD=1
|
20 |
+
shift
|
21 |
+
;;
|
22 |
+
--service)
|
23 |
+
SERVICE="$2"
|
24 |
+
shift 2
|
25 |
+
;;
|
26 |
+
--help)
|
27 |
+
echo "用法: ./build_docker.sh [选项]"
|
28 |
+
echo "选项:"
|
29 |
+
echo " --clean 清理缓存目录"
|
30 |
+
echo " --rebuild 强制重新构建镜像"
|
31 |
+
echo " --service 指定要构建的服务名称"
|
32 |
+
echo " --help 显示此帮助信息"
|
33 |
+
exit 0
|
34 |
+
;;
|
35 |
+
*)
|
36 |
+
echo "未知选项: $1"
|
37 |
+
echo "使用 --help 查看帮助"
|
38 |
+
exit 1
|
39 |
+
;;
|
40 |
+
esac
|
41 |
+
done
|
42 |
+
|
43 |
+
# 检测操作系统类型
|
44 |
+
OS_TYPE=$(uname -s)
|
45 |
+
echo "检测到操作系统: $OS_TYPE"
|
46 |
+
|
47 |
+
# 检查Docker是否安装
|
48 |
+
if ! command -v docker &> /dev/null; then
|
49 |
+
echo "错误: Docker未安装"
|
50 |
+
echo "请先安装Docker: https://docs.docker.com/get-docker/"
|
51 |
+
exit 1
|
52 |
+
fi
|
53 |
+
|
54 |
+
# 检查Docker是否运行
|
55 |
+
if ! docker info &> /dev/null; then
|
56 |
+
echo "错误: Docker未运行"
|
57 |
+
echo "请启动Docker服务"
|
58 |
+
exit 1
|
59 |
+
fi
|
60 |
+
|
61 |
+
# 检查docker-compose.yml文件是否存在
|
62 |
+
if [ ! -f "$COMPOSE_FILE" ]; then
|
63 |
+
echo "错误: 未找到$COMPOSE_FILE文件"
|
64 |
+
echo "请确保在正确的目录中运行此脚本"
|
65 |
+
exit 1
|
66 |
+
fi
|
67 |
+
|
68 |
+
# 设置Docker BuildKit环境变量
|
69 |
+
export DOCKER_BUILDKIT=1
|
70 |
+
export COMPOSE_DOCKER_CLI_BUILD=1
|
71 |
+
|
72 |
+
echo "启用Docker BuildKit加速构建..."
|
73 |
+
|
74 |
+
# 清理缓存(如果指定)
|
75 |
+
if [ $CLEAN_CACHE -eq 1 ]; then
|
76 |
+
echo "清理缓存目录..."
|
77 |
+
rm -rf "$CACHE_DIR"
|
78 |
+
fi
|
79 |
+
|
80 |
+
# 创建缓存目录
|
81 |
+
mkdir -p "$CACHE_DIR"
|
82 |
+
|
83 |
+
# 添加构建时间标记
|
84 |
+
BUILD_TIME=$(date +"%Y%m%d_%H%M%S")
|
85 |
+
BUILD_ARGS="$BUILD_ARGS --build-arg BUILD_TIME=$BUILD_TIME"
|
86 |
+
|
87 |
+
# 获取脚本所在目录
|
88 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
89 |
+
# 获取项目根目录(脚本所在目录的父目录)
|
90 |
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
91 |
+
|
92 |
+
echo "脚本目录: $SCRIPT_DIR"
|
93 |
+
echo "项目根目录: $PROJECT_ROOT"
|
94 |
+
|
95 |
+
# 切换到项目根目录
|
96 |
+
cd "$PROJECT_ROOT"
|
97 |
+
|
98 |
+
# 检查Docker Compose命令
|
99 |
+
if command -v docker-compose &> /dev/null; then
|
100 |
+
COMPOSE_CMD="docker-compose"
|
101 |
+
echo "使用 docker-compose 命令"
|
102 |
+
elif docker compose version &> /dev/null; then
|
103 |
+
COMPOSE_CMD="docker compose"
|
104 |
+
echo "使用 docker compose 命令"
|
105 |
+
else
|
106 |
+
echo "错误: 未找到Docker Compose命令"
|
107 |
+
echo "请安装Docker Compose: https://docs.docker.com/compose/install/"
|
108 |
+
exit 1
|
109 |
+
fi
|
110 |
+
|
111 |
+
# 检测CPU核心数,用于并行构建
|
112 |
+
CPU_CORES=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)
|
113 |
+
if [ $CPU_CORES -gt 2 ]; then
|
114 |
+
PARALLEL_FLAG="--parallel"
|
115 |
+
echo "检测到${CPU_CORES}个CPU核心,启用并行构建..."
|
116 |
+
else
|
117 |
+
PARALLEL_FLAG=""
|
118 |
+
fi
|
119 |
+
|
120 |
+
# 构建命令基础部分
|
121 |
+
BUILD_CMD="$COMPOSE_CMD -f \"$SCRIPT_DIR/docker-compose.yml\" build $PARALLEL_FLAG --build-arg BUILDKIT_INLINE_CACHE=1"
|
122 |
+
|
123 |
+
# 根据操作系统类型执行不同的命令
|
124 |
+
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
125 |
+
# macOS
|
126 |
+
echo "在macOS上构建Docker镜像..."
|
127 |
+
eval $BUILD_CMD
|
128 |
+
elif [[ "$OS_TYPE" == "Linux" ]]; then
|
129 |
+
# Linux
|
130 |
+
echo "在Linux上构建Docker镜像..."
|
131 |
+
eval $BUILD_CMD
|
132 |
+
elif [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
133 |
+
# Windows
|
134 |
+
echo "在Windows上构建Docker镜像..."
|
135 |
+
eval $BUILD_CMD
|
136 |
+
else
|
137 |
+
echo "未知操作系统,尝试使用标准命令构建..."
|
138 |
+
eval $BUILD_CMD
|
139 |
+
fi
|
140 |
+
|
141 |
+
# 检查构建结果
|
142 |
+
if [ $? -eq 0 ]; then
|
143 |
+
echo "Docker镜像构建成功!"
|
144 |
+
echo "构建时间: $BUILD_TIME"
|
145 |
+
echo "可以使用以下命令启动容器:"
|
146 |
+
echo "$COMPOSE_CMD -f \"$SCRIPT_DIR/docker-compose.yml\" up -d"
|
147 |
+
else
|
148 |
+
echo "Docker镜像构建失败,请检查错误信息。"
|
149 |
+
exit 1
|
150 |
+
fi
|
.container/check_docker.bat
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
echo 检查Docker环境...
|
3 |
+
|
4 |
+
REM 检查Docker是否安装
|
5 |
+
where docker >nul 2>nul
|
6 |
+
if %ERRORLEVEL% NEQ 0 (
|
7 |
+
echo 错误: Docker未安装
|
8 |
+
echo 在Windows上安装Docker的方法:
|
9 |
+
echo 1. 访问 https://docs.docker.com/desktop/install/windows-install/ 下载Docker Desktop
|
10 |
+
echo 2. 安装并启动Docker Desktop
|
11 |
+
pause
|
12 |
+
exit /b 1
|
13 |
+
)
|
14 |
+
|
15 |
+
echo Docker已安装
|
16 |
+
|
17 |
+
REM 检查Docker Compose是否安装
|
18 |
+
where docker-compose >nul 2>nul
|
19 |
+
if %ERRORLEVEL% NEQ 0 (
|
20 |
+
echo 警告: Docker-Compose未找到,尝试使用新的docker compose命令
|
21 |
+
docker compose version >nul 2>nul
|
22 |
+
if %ERRORLEVEL% NEQ 0 (
|
23 |
+
echo 错误: Docker Compose未安装
|
24 |
+
echo Docker Desktop for Windows应该已包含Docker Compose
|
25 |
+
echo 请确保Docker Desktop已正确安装
|
26 |
+
pause
|
27 |
+
exit /b 1
|
28 |
+
) else (
|
29 |
+
echo 使用新的docker compose命令
|
30 |
+
set COMPOSE_CMD=docker compose
|
31 |
+
)
|
32 |
+
) else (
|
33 |
+
echo Docker-Compose已安装
|
34 |
+
set COMPOSE_CMD=docker-compose
|
35 |
+
)
|
36 |
+
|
37 |
+
REM 检查Docker是否正在运行
|
38 |
+
docker info >nul 2>nul
|
39 |
+
if %ERRORLEVEL% NEQ 0 (
|
40 |
+
echo 错误: Docker未运行
|
41 |
+
echo 请启动Docker Desktop应用程序
|
42 |
+
pause
|
43 |
+
exit /b 1
|
44 |
+
)
|
45 |
+
|
46 |
+
echo Docker正在运行
|
47 |
+
|
48 |
+
REM 检查是否有.env文件
|
49 |
+
if not exist "owl\.env" (
|
50 |
+
echo 警告: 未找到owl\.env文件
|
51 |
+
echo 请运行以下命令创建环境变量文件:
|
52 |
+
echo copy owl\.env_template owl\.env
|
53 |
+
echo 然后编辑owl\.env文件,填写必要的API密钥
|
54 |
+
) else (
|
55 |
+
echo 环境变量文件已存在
|
56 |
+
)
|
57 |
+
|
58 |
+
echo 所有检查完成,您的系统已准备好构建和运行OWL项目的Docker容器
|
59 |
+
echo 请运行以下命令构建Docker镜像:
|
60 |
+
echo %COMPOSE_CMD% build
|
61 |
+
|
62 |
+
pause
|
.container/check_docker.sh
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# 检测操作系统类型
|
4 |
+
OS_TYPE=$(uname -s)
|
5 |
+
echo "检测到操作系统: $OS_TYPE"
|
6 |
+
|
7 |
+
# 检查Docker是否安装
|
8 |
+
if ! command -v docker &> /dev/null; then
|
9 |
+
echo "错误: Docker未安装"
|
10 |
+
|
11 |
+
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
12 |
+
echo "在macOS上安装Docker的方法:"
|
13 |
+
echo "1. 访问 https://docs.docker.com/desktop/install/mac-install/ 下载Docker Desktop"
|
14 |
+
echo "2. 安装并启动Docker Desktop"
|
15 |
+
elif [[ "$OS_TYPE" == "Linux" ]]; then
|
16 |
+
echo "在Linux上安装Docker的方法:"
|
17 |
+
echo "1. 运行以下命令:"
|
18 |
+
echo " sudo apt-get update"
|
19 |
+
echo " sudo apt-get install docker.io docker-compose"
|
20 |
+
echo "2. 启动Docker服务:"
|
21 |
+
echo " sudo systemctl start docker"
|
22 |
+
echo " sudo systemctl enable docker"
|
23 |
+
elif [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
24 |
+
echo "在Windows上安装Docker的方法:"
|
25 |
+
echo "1. 访问 https://docs.docker.com/desktop/install/windows-install/ 下载Docker Desktop"
|
26 |
+
echo "2. 安装并启动Docker Desktop"
|
27 |
+
fi
|
28 |
+
|
29 |
+
exit 1
|
30 |
+
fi
|
31 |
+
|
32 |
+
echo "Docker已安装"
|
33 |
+
|
34 |
+
# 检查Docker Compose是否安装
|
35 |
+
if ! command -v docker-compose &> /dev/null; then
|
36 |
+
echo "错误: Docker Compose未安装"
|
37 |
+
|
38 |
+
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
39 |
+
echo "Docker Desktop for Mac已包含Docker Compose"
|
40 |
+
elif [[ "$OS_TYPE" == "Linux" ]]; then
|
41 |
+
echo "在Linux上安装Docker Compose的方法:"
|
42 |
+
echo "1. 运行以下命令:"
|
43 |
+
echo " sudo apt-get install docker-compose"
|
44 |
+
elif [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
45 |
+
echo "Docker Desktop for Windows已包含Docker Compose"
|
46 |
+
fi
|
47 |
+
|
48 |
+
exit 1
|
49 |
+
fi
|
50 |
+
|
51 |
+
echo "Docker Compose已安装"
|
52 |
+
|
53 |
+
# 检查Docker是否正在运行
|
54 |
+
if ! docker info &> /dev/null; then
|
55 |
+
echo "错误: Docker未运行"
|
56 |
+
|
57 |
+
if [[ "$OS_TYPE" == "Darwin" ]]; then
|
58 |
+
echo "请启动Docker Desktop应用程序"
|
59 |
+
elif [[ "$OS_TYPE" == "Linux" ]]; then
|
60 |
+
echo "请运行以下命令启动Docker服务:"
|
61 |
+
echo "sudo systemctl start docker"
|
62 |
+
elif [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
63 |
+
echo "请启动Docker Desktop应用程序"
|
64 |
+
fi
|
65 |
+
|
66 |
+
exit 1
|
67 |
+
fi
|
68 |
+
|
69 |
+
echo "Docker正在运行"
|
70 |
+
|
71 |
+
# 检查是否有足够的磁盘空间
|
72 |
+
FREE_SPACE=$(df -h . | awk 'NR==2 {print $4}')
|
73 |
+
echo "可用磁盘空间: $FREE_SPACE"
|
74 |
+
|
75 |
+
# 检查是否有.env文件
|
76 |
+
if [ ! -f "owl/.env" ]; then
|
77 |
+
echo "警告: 未找到owl/.env文件"
|
78 |
+
echo "请运行以下命令创建环境变量文件:"
|
79 |
+
echo "cp owl/.env_template owl/.env"
|
80 |
+
echo "然后编辑owl/.env文件,填写必要的API密钥"
|
81 |
+
else
|
82 |
+
echo "环境变量文件已存在"
|
83 |
+
fi
|
84 |
+
|
85 |
+
echo "所有检查完成,您的系统已准备好构建和运行OWL项目的Docker容器"
|
86 |
+
echo "请运行以下命令构建Docker镜像:"
|
87 |
+
|
88 |
+
if [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
89 |
+
echo "build_docker.bat"
|
90 |
+
else
|
91 |
+
echo "./build_docker.sh"
|
92 |
+
fi
|
.container/docker-compose.yml
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
owl:
|
3 |
+
build:
|
4 |
+
context: ..
|
5 |
+
dockerfile: .container/Dockerfile
|
6 |
+
args:
|
7 |
+
# 构建参数
|
8 |
+
BUILDKIT_INLINE_CACHE: 1
|
9 |
+
# 使用BuildKit加速构建
|
10 |
+
cache_from:
|
11 |
+
- python:3.10-slim
|
12 |
+
volumes:
|
13 |
+
# 挂载.env文件,方便配置API密钥
|
14 |
+
- ./owl/.env:/app/owl/.env
|
15 |
+
# 可选:挂载数据目录
|
16 |
+
- ./data:/app/data
|
17 |
+
# 挂载缓存目录,避免重复下载
|
18 |
+
- playwright-cache:/root/.cache/ms-playwright
|
19 |
+
- pip-cache:/root/.pip/cache
|
20 |
+
environment:
|
21 |
+
# 可以在这里设置环境变量,覆盖.env文件中的设置
|
22 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
23 |
+
# 添加显示相关的环境变量
|
24 |
+
- DISPLAY=:99
|
25 |
+
- PLAYWRIGHT_BROWSERS_PATH=/root/.cache/ms-playwright
|
26 |
+
# 设置Python不生成.pyc文件,减少磁盘IO
|
27 |
+
- PYTHONDONTWRITEBYTECODE=1
|
28 |
+
# 设置Python不缓冲输出,方便查看日志
|
29 |
+
- PYTHONUNBUFFERED=1
|
30 |
+
# 设置终端颜色
|
31 |
+
- TERM=xterm-256color
|
32 |
+
# 启用pip缓存
|
33 |
+
- PIP_CACHE_DIR=/root/.pip/cache
|
34 |
+
ports:
|
35 |
+
# 如果项目有Web界面,可以映射端口
|
36 |
+
- "8000:8000"
|
37 |
+
# 使用交互模式运行容器
|
38 |
+
stdin_open: true
|
39 |
+
tty: true
|
40 |
+
# 添加共享内存大小,提高浏览器性能
|
41 |
+
shm_size: 2gb
|
42 |
+
# 设置资源限制
|
43 |
+
deploy:
|
44 |
+
resources:
|
45 |
+
limits:
|
46 |
+
cpus: '2'
|
47 |
+
memory: 4G
|
48 |
+
|
49 |
+
# 定义持久化卷,用于缓存
|
50 |
+
volumes:
|
51 |
+
playwright-cache:
|
52 |
+
pip-cache:
|
.container/run_in_docker.bat
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
setlocal enabledelayedexpansion
|
3 |
+
|
4 |
+
REM 定义配置变量
|
5 |
+
set SERVICE_NAME=owl
|
6 |
+
set PYTHON_CMD=xvfb-python
|
7 |
+
set MAX_WAIT_SECONDS=60
|
8 |
+
set CHECK_INTERVAL_SECONDS=2
|
9 |
+
|
10 |
+
REM 检查参数
|
11 |
+
if "%~1"=="" (
|
12 |
+
echo 用法: run_in_docker.bat [脚本名称] "你的问题"
|
13 |
+
echo 例如: run_in_docker.bat run.py "什么是人工智能?"
|
14 |
+
echo 或者: run_in_docker.bat run_deepseek_example.py "什么是人工智能?"
|
15 |
+
echo 如果不指定脚本名称,默认使用 run.py
|
16 |
+
exit /b 1
|
17 |
+
)
|
18 |
+
|
19 |
+
REM 判断第一个参数是否是脚本名称
|
20 |
+
set SCRIPT_NAME=%~1
|
21 |
+
set QUERY=%~2
|
22 |
+
|
23 |
+
if "!SCRIPT_NAME:~-3!"==".py" (
|
24 |
+
REM 如果提供了第二个参数,则为查询内容
|
25 |
+
if "!QUERY!"=="" (
|
26 |
+
echo 请提供查询参数,例如: run_in_docker.bat !SCRIPT_NAME! "你的问题"
|
27 |
+
exit /b 1
|
28 |
+
)
|
29 |
+
) else (
|
30 |
+
REM 如果第一个参数不是脚本名称,则默认使用 run.py
|
31 |
+
set QUERY=!SCRIPT_NAME!
|
32 |
+
set SCRIPT_NAME=run.py
|
33 |
+
)
|
34 |
+
|
35 |
+
REM 检查脚本是否存在
|
36 |
+
if not exist "owl\!SCRIPT_NAME!" (
|
37 |
+
echo 错误: 脚本 'owl\!SCRIPT_NAME!' 不存在
|
38 |
+
echo 可用的脚本有:
|
39 |
+
dir /b owl\*.py | findstr /v "__"
|
40 |
+
exit /b 1
|
41 |
+
)
|
42 |
+
|
43 |
+
echo 使用脚本: !SCRIPT_NAME!
|
44 |
+
echo 查询内容: !QUERY!
|
45 |
+
|
46 |
+
REM 从docker-compose.yml获取服务名称(如果文件存在)
|
47 |
+
if exist ".container\docker-compose.yml" (
|
48 |
+
for /f "tokens=*" %%a in ('findstr /r "^ [a-zA-Z0-9_-]*:" .container\docker-compose.yml') do (
|
49 |
+
set line=%%a
|
50 |
+
set service=!line:~2,-1!
|
51 |
+
if not "!service!"=="" (
|
52 |
+
REM 使用第一个找到的服务名称
|
53 |
+
set SERVICE_NAME=!service!
|
54 |
+
echo 从docker-compose.yml检测到服务名称: !SERVICE_NAME!
|
55 |
+
goto :found_service
|
56 |
+
)
|
57 |
+
)
|
58 |
+
)
|
59 |
+
:found_service
|
60 |
+
|
61 |
+
REM 确保Docker容器正在运行
|
62 |
+
docker-compose ps | findstr "!SERVICE_NAME!.*Up" > nul
|
63 |
+
if errorlevel 1 (
|
64 |
+
echo 启动Docker容器...
|
65 |
+
docker-compose up -d
|
66 |
+
|
67 |
+
REM 使用循环检查容器是否就绪
|
68 |
+
echo 等待容器启动...
|
69 |
+
set /a total_wait=0
|
70 |
+
|
71 |
+
:wait_loop
|
72 |
+
timeout /t !CHECK_INTERVAL_SECONDS! /nobreak > nul
|
73 |
+
set /a total_wait+=!CHECK_INTERVAL_SECONDS!
|
74 |
+
|
75 |
+
docker-compose ps | findstr "!SERVICE_NAME!.*Up" > nul
|
76 |
+
if errorlevel 1 (
|
77 |
+
if !total_wait! LSS !MAX_WAIT_SECONDS! (
|
78 |
+
echo 容器尚未就绪,已等待!total_wait!秒,继续等待...
|
79 |
+
goto :wait_loop
|
80 |
+
) else (
|
81 |
+
echo 错误:容器启动超时,已等待!MAX_WAIT_SECONDS!秒
|
82 |
+
echo 请检查Docker容器状态:docker-compose ps
|
83 |
+
exit /b 1
|
84 |
+
)
|
85 |
+
) else (
|
86 |
+
echo 容器已就绪,共等待了!total_wait!秒
|
87 |
+
)
|
88 |
+
)
|
89 |
+
|
90 |
+
REM 检查容器中是否存在xvfb-python命令
|
91 |
+
echo 检查容器中的命令...
|
92 |
+
docker-compose exec -T !SERVICE_NAME! which !PYTHON_CMD! > nul 2>&1
|
93 |
+
if errorlevel 1 (
|
94 |
+
echo 警告:容器中未找到!PYTHON_CMD!命令,尝试使用python替代
|
95 |
+
set PYTHON_CMD=python
|
96 |
+
|
97 |
+
REM 检查python命令是否存在
|
98 |
+
docker-compose exec -T !SERVICE_NAME! which python > nul 2>&1
|
99 |
+
if errorlevel 1 (
|
100 |
+
echo 错误:容器中未找到python命令
|
101 |
+
echo 请检查容器配置
|
102 |
+
exit /b 1
|
103 |
+
)
|
104 |
+
)
|
105 |
+
|
106 |
+
REM 在容器中运行指定的脚本,传递查询参数
|
107 |
+
echo 在Docker容器中使用!PYTHON_CMD!运行脚本...
|
108 |
+
docker-compose exec -T !SERVICE_NAME! !PYTHON_CMD! !SCRIPT_NAME! "!QUERY!"
|
109 |
+
|
110 |
+
if errorlevel 0 (
|
111 |
+
echo 查询完成!
|
112 |
+
) else (
|
113 |
+
echo 查询执行失败,请检查错误信息。
|
114 |
+
)
|
115 |
+
|
116 |
+
pause
|
.container/run_in_docker.sh
ADDED
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# 定义配置变量
|
4 |
+
SERVICE_NAME="owl"
|
5 |
+
PYTHON_CMD="xvfb-python"
|
6 |
+
MAX_WAIT_SECONDS=60
|
7 |
+
CHECK_INTERVAL_SECONDS=2
|
8 |
+
|
9 |
+
# 检测操作系统类型
|
10 |
+
OS_TYPE=$(uname -s)
|
11 |
+
echo "检测到操作系统: $OS_TYPE"
|
12 |
+
|
13 |
+
# 检查是否提供了查询参数
|
14 |
+
if [ $# -lt 1 ]; then
|
15 |
+
echo "用法: ./run_in_docker.sh [脚本名称] '你的问题'"
|
16 |
+
echo "例如: ./run_in_docker.sh run.py '什么是人工智能?'"
|
17 |
+
echo "或者: ./run_in_docker.sh run_deepseek_example.py '什么是人工智能?'"
|
18 |
+
echo "如果不指定脚本名称,默认使用 run.py"
|
19 |
+
exit 1
|
20 |
+
fi
|
21 |
+
|
22 |
+
# 判断第一个参数是否是脚本名称
|
23 |
+
if [[ $1 == *.py ]]; then
|
24 |
+
SCRIPT_NAME="$1"
|
25 |
+
# 如果提供了第二个参数,则为查询内容
|
26 |
+
if [ $# -ge 2 ]; then
|
27 |
+
QUERY="$2"
|
28 |
+
else
|
29 |
+
echo "请提供查询参数,例如: ./run_in_docker.sh $SCRIPT_NAME '你的问题'"
|
30 |
+
exit 1
|
31 |
+
fi
|
32 |
+
else
|
33 |
+
# 如果第一个参数不是脚本名称,则默认使用 run.py
|
34 |
+
SCRIPT_NAME="run.py"
|
35 |
+
QUERY="$1"
|
36 |
+
fi
|
37 |
+
|
38 |
+
# 检查脚本是否存在
|
39 |
+
if [ ! -f "owl/$SCRIPT_NAME" ]; then
|
40 |
+
echo "错误: 脚本 'owl/$SCRIPT_NAME' 不存在"
|
41 |
+
echo "可用的脚本有:"
|
42 |
+
if [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
43 |
+
find owl -name "*.py" | grep -v "__" | sed 's/\\/\//g'
|
44 |
+
else
|
45 |
+
ls -1 owl/*.py | grep -v "__"
|
46 |
+
fi
|
47 |
+
exit 1
|
48 |
+
fi
|
49 |
+
|
50 |
+
echo "使用脚本: $SCRIPT_NAME"
|
51 |
+
echo "查询内容: $QUERY"
|
52 |
+
|
53 |
+
# 从docker-compose.yml获取服务名称(如果文件存在)
|
54 |
+
if [ -f ".container/docker-compose.yml" ]; then
|
55 |
+
DETECTED_SERVICE=$(grep -E "^ [a-zA-Z0-9_-]*:" .container/docker-compose.yml | head -1 | sed 's/^ \(.*\):.*/\1/')
|
56 |
+
if [ ! -z "$DETECTED_SERVICE" ]; then
|
57 |
+
SERVICE_NAME="$DETECTED_SERVICE"
|
58 |
+
echo "从docker-compose.yml检测到服务名称: $SERVICE_NAME"
|
59 |
+
fi
|
60 |
+
fi
|
61 |
+
|
62 |
+
# 检查Docker Compose命令
|
63 |
+
if command -v docker-compose &> /dev/null; then
|
64 |
+
COMPOSE_CMD="docker-compose"
|
65 |
+
elif docker compose version &> /dev/null; then
|
66 |
+
COMPOSE_CMD="docker compose"
|
67 |
+
else
|
68 |
+
echo "错误: 未找到Docker Compose命令"
|
69 |
+
exit 1
|
70 |
+
fi
|
71 |
+
|
72 |
+
# 确保Docker容器正在运行
|
73 |
+
CONTAINER_RUNNING=$($COMPOSE_CMD ps | grep -c "$SERVICE_NAME.*Up" || true)
|
74 |
+
if [ "$CONTAINER_RUNNING" -eq 0 ]; then
|
75 |
+
echo "启动Docker容器..."
|
76 |
+
$COMPOSE_CMD up -d
|
77 |
+
|
78 |
+
# 使用循环检查容器是否就绪
|
79 |
+
echo "等待容器启动..."
|
80 |
+
TOTAL_WAIT=0
|
81 |
+
|
82 |
+
while [ $TOTAL_WAIT -lt $MAX_WAIT_SECONDS ]; do
|
83 |
+
sleep $CHECK_INTERVAL_SECONDS
|
84 |
+
TOTAL_WAIT=$((TOTAL_WAIT + CHECK_INTERVAL_SECONDS))
|
85 |
+
|
86 |
+
CONTAINER_RUNNING=$($COMPOSE_CMD ps | grep -c "$SERVICE_NAME.*Up" || true)
|
87 |
+
if [ "$CONTAINER_RUNNING" -gt 0 ]; then
|
88 |
+
echo "容器已就绪,共等待了 $TOTAL_WAIT 秒"
|
89 |
+
break
|
90 |
+
else
|
91 |
+
echo "容器尚未就绪,已等待 $TOTAL_WAIT 秒,继续等待..."
|
92 |
+
fi
|
93 |
+
done
|
94 |
+
|
95 |
+
if [ "$CONTAINER_RUNNING" -eq 0 ]; then
|
96 |
+
echo "错误:容器启动超时,已等待 $MAX_WAIT_SECONDS 秒"
|
97 |
+
echo "请检查Docker容器状态:$COMPOSE_CMD ps"
|
98 |
+
exit 1
|
99 |
+
fi
|
100 |
+
fi
|
101 |
+
|
102 |
+
# 检查容器中是否存在指定的Python命令
|
103 |
+
echo "检查容器中的命令..."
|
104 |
+
if ! $COMPOSE_CMD exec -T $SERVICE_NAME which $PYTHON_CMD &> /dev/null; then
|
105 |
+
echo "警告:容器中未找到 $PYTHON_CMD 命令,尝试使用python替代"
|
106 |
+
PYTHON_CMD="python"
|
107 |
+
|
108 |
+
# 检查python命令是否存在
|
109 |
+
if ! $COMPOSE_CMD exec -T $SERVICE_NAME which python &> /dev/null; then
|
110 |
+
echo "错误:容器中未找到python命令"
|
111 |
+
echo "请检查容器配置"
|
112 |
+
exit 1
|
113 |
+
fi
|
114 |
+
fi
|
115 |
+
|
116 |
+
# 在容器中运行指定的脚本,传递查询参数
|
117 |
+
echo "在Docker容器中使用 $PYTHON_CMD 运行脚本..."
|
118 |
+
|
119 |
+
# 根据操作系统类型执行不同的命令
|
120 |
+
if [[ "$OS_TYPE" == MINGW* ]] || [[ "$OS_TYPE" == CYGWIN* ]] || [[ "$OS_TYPE" == MSYS* ]]; then
|
121 |
+
# Windows可能需要特殊处理引号
|
122 |
+
winpty $COMPOSE_CMD exec -T $SERVICE_NAME $PYTHON_CMD $SCRIPT_NAME "$QUERY"
|
123 |
+
RESULT=$?
|
124 |
+
else
|
125 |
+
# macOS 或 Linux
|
126 |
+
$COMPOSE_CMD exec -T $SERVICE_NAME $PYTHON_CMD $SCRIPT_NAME "$QUERY"
|
127 |
+
RESULT=$?
|
128 |
+
fi
|
129 |
+
|
130 |
+
# 检查命令执行结果
|
131 |
+
if [ $RESULT -eq 0 ]; then
|
132 |
+
echo "查询完成!"
|
133 |
+
else
|
134 |
+
echo "查询执行失败,请检查错误信息。"
|
135 |
+
fi
|
.gitattributes
CHANGED
@@ -6,3 +6,11 @@ owl-main/assets/community.png filter=lfs diff=lfs merge=lfs -text
|
|
6 |
owl-main/assets/meetup.jpg filter=lfs diff=lfs merge=lfs -text
|
7 |
owl-main/assets/owl_architecture.png filter=lfs diff=lfs merge=lfs -text
|
8 |
owl-main/assets/qr_code.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
owl-main/assets/meetup.jpg filter=lfs diff=lfs merge=lfs -text
|
7 |
owl-main/assets/owl_architecture.png filter=lfs diff=lfs merge=lfs -text
|
8 |
owl-main/assets/qr_code.jpg filter=lfs diff=lfs merge=lfs -text
|
9 |
+
assets/community_2.png filter=lfs diff=lfs merge=lfs -text
|
10 |
+
assets/community_3.jpg filter=lfs diff=lfs merge=lfs -text
|
11 |
+
assets/community_4.jpg filter=lfs diff=lfs merge=lfs -text
|
12 |
+
assets/community_5.jpg filter=lfs diff=lfs merge=lfs -text
|
13 |
+
assets/community.png filter=lfs diff=lfs merge=lfs -text
|
14 |
+
assets/meetup.jpg filter=lfs diff=lfs merge=lfs -text
|
15 |
+
assets/owl_architecture.png filter=lfs diff=lfs merge=lfs -text
|
16 |
+
assets/qr_code.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
**/__pycache__/
|
4 |
+
*/__pycache__/*
|
5 |
+
*.py[cod]
|
6 |
+
*$py.class
|
7 |
+
*.so
|
8 |
+
.Python
|
9 |
+
build/
|
10 |
+
develop-eggs/
|
11 |
+
.dist
|
12 |
+
downloads/
|
13 |
+
eggs/
|
14 |
+
.eggs/
|
15 |
+
lib/
|
16 |
+
lib64/
|
17 |
+
parts/
|
18 |
+
sdist/
|
19 |
+
var/
|
20 |
+
wheels/
|
21 |
+
*.egg-info/
|
22 |
+
.installed.cfg
|
23 |
+
*.egg
|
24 |
+
|
25 |
+
# Virtual Environment
|
26 |
+
venv/
|
27 |
+
env/
|
28 |
+
ENV/
|
29 |
+
.env
|
30 |
+
|
31 |
+
# IDE
|
32 |
+
.idea/
|
33 |
+
.vscode/
|
34 |
+
*.swp
|
35 |
+
*.swo
|
36 |
+
.DS_Store
|
37 |
+
|
38 |
+
# Project specific
|
39 |
+
owl/data
|
40 |
+
owl/tmp
|
41 |
+
owl/.env
|
42 |
+
owl/utils/__pycache__/
|
43 |
+
|
44 |
+
# Logs
|
45 |
+
*.log
|
46 |
+
logs/
|
47 |
+
log/
|
48 |
+
|
49 |
+
# Coverage reports
|
50 |
+
htmlcov/
|
51 |
+
.tox/
|
52 |
+
.coverage
|
53 |
+
.coverage.*
|
54 |
+
.cache
|
55 |
+
coverage.xml
|
56 |
+
*.cover
|
57 |
+
|
58 |
+
owl/camel/types/__pycache__/
|
59 |
+
owl/camel/__pycache__/
|
60 |
+
owl/camel/utils/__pycache_/
|
DOCKER_README_en.md
ADDED
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# OWL Project Docker Usage Guide
|
2 |
+
|
3 |
+
This document provides detailed instructions on how to run the OWL project using Docker.
|
4 |
+
|
5 |
+
## Prerequisites
|
6 |
+
|
7 |
+
• Install [Docker](https://docs.docker.com/get-docker/)
|
8 |
+
• Install [Docker Compose](https://docs.docker.com/compose/install/) (recommended v2.x version)
|
9 |
+
• Obtain necessary API keys (OpenAI API, etc.)
|
10 |
+
|
11 |
+
## Technical Notes
|
12 |
+
|
13 |
+
This Docker configuration uses the following technologies to ensure the OWL project runs smoothly in containers:
|
14 |
+
|
15 |
+
• **Xvfb**: Virtual framebuffer, used to simulate an X server in a headless environment
|
16 |
+
• **Playwright**: Used for browser automation, configured in headless mode
|
17 |
+
• **Shared Memory**: Increased shared memory size to improve browser performance
|
18 |
+
• **BuildKit**: Uses Docker BuildKit to accelerate the build process
|
19 |
+
• **Cache Optimization**: Uses persistent volumes to cache pip and Playwright dependencies
|
20 |
+
• **Cross-Platform Compatibility**: Provides scripts for both Windows and macOS/Linux
|
21 |
+
|
22 |
+
## Docker Compose Version Notes
|
23 |
+
|
24 |
+
The docker-compose.yml file used in this project is compatible with Docker Compose v2.x. If you are using an older Docker Compose v1.x version, you may need to manually add the version number:
|
25 |
+
|
26 |
+
```yaml
|
27 |
+
version: '3'
|
28 |
+
|
29 |
+
services:
|
30 |
+
# ...rest of the configuration remains unchanged
|
31 |
+
```
|
32 |
+
|
33 |
+
## Quick Start
|
34 |
+
|
35 |
+
### 0. Check Environment
|
36 |
+
|
37 |
+
First, run the check script to ensure your environment is ready:
|
38 |
+
|
39 |
+
#### Check on macOS/Linux
|
40 |
+
|
41 |
+
```bash
|
42 |
+
# First, add execute permissions to the script
|
43 |
+
chmod +x check_docker.sh
|
44 |
+
|
45 |
+
# Run the check script
|
46 |
+
./check_docker.sh
|
47 |
+
```
|
48 |
+
|
49 |
+
#### Check on Windows
|
50 |
+
|
51 |
+
```cmd
|
52 |
+
check_docker.bat
|
53 |
+
```
|
54 |
+
|
55 |
+
If the check script finds any issues, please follow the prompts to fix them.
|
56 |
+
|
57 |
+
### 1. Configure Environment Variables
|
58 |
+
|
59 |
+
Copy the environment variable template file and fill in the necessary API keys:
|
60 |
+
|
61 |
+
```bash
|
62 |
+
cp owl/.env_template owl/.env
|
63 |
+
```
|
64 |
+
|
65 |
+
Then edit the `owl/.env` file and fill in the necessary API keys, for example:
|
66 |
+
|
67 |
+
```
|
68 |
+
OPENAI_API_KEY=your_openai_api_key
|
69 |
+
GOOGLE_API_KEY=your_google_api_key
|
70 |
+
SEARCH_ENGINE_ID=your_search_engine_id
|
71 |
+
```
|
72 |
+
|
73 |
+
### 2. Quick Build Docker Image
|
74 |
+
|
75 |
+
#### Build on macOS/Linux
|
76 |
+
|
77 |
+
Use the provided shell script to speed up the Docker image build:
|
78 |
+
|
79 |
+
```bash
|
80 |
+
# First, add execute permissions to the script
|
81 |
+
chmod +x build_docker.sh
|
82 |
+
|
83 |
+
# Run the build script
|
84 |
+
./build_docker.sh
|
85 |
+
```
|
86 |
+
|
87 |
+
#### Build on Windows
|
88 |
+
|
89 |
+
Use the provided batch file:
|
90 |
+
|
91 |
+
```cmd
|
92 |
+
build_docker.bat
|
93 |
+
```
|
94 |
+
|
95 |
+
Or build and start using the standard method:
|
96 |
+
|
97 |
+
```bash
|
98 |
+
# Use BuildKit to accelerate the build
|
99 |
+
set DOCKER_BUILDKIT=1
|
100 |
+
set COMPOSE_DOCKER_CLI_BUILD=1
|
101 |
+
docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1
|
102 |
+
|
103 |
+
# Start the container
|
104 |
+
docker-compose up -d
|
105 |
+
```
|
106 |
+
|
107 |
+
### 3. Interactive Use of the Container
|
108 |
+
|
109 |
+
After the container starts, it will automatically enter an interactive shell environment and display a welcome message and a list of available scripts:
|
110 |
+
|
111 |
+
```bash
|
112 |
+
# Enter the container (if not automatically entered)
|
113 |
+
docker-compose exec owl bash
|
114 |
+
```
|
115 |
+
|
116 |
+
Inside the container, you can directly run any available script:
|
117 |
+
|
118 |
+
```bash
|
119 |
+
# Run the default script
|
120 |
+
xvfb-python run.py
|
121 |
+
|
122 |
+
# Run the DeepSeek example
|
123 |
+
xvfb-python run_deepseek_example.py
|
124 |
+
|
125 |
+
# Run the script and pass query parameters
|
126 |
+
xvfb-python run.py "What is artificial intelligence?"
|
127 |
+
```
|
128 |
+
|
129 |
+
### 4. Run Queries Using External Scripts
|
130 |
+
|
131 |
+
#### Run on macOS/Linux
|
132 |
+
|
133 |
+
```bash
|
134 |
+
# First, add execute permissions to the script
|
135 |
+
chmod +x run_in_docker.sh
|
136 |
+
|
137 |
+
# Default to using the run.py script
|
138 |
+
./run_in_docker.sh "your question"
|
139 |
+
|
140 |
+
# Specify a particular script
|
141 |
+
./run_in_docker.sh run_deepseek_example.py "your question"
|
142 |
+
```
|
143 |
+
|
144 |
+
#### Run on Windows
|
145 |
+
|
146 |
+
```cmd
|
147 |
+
REM Default to using the run.py script
|
148 |
+
run_in_docker.bat "your question"
|
149 |
+
|
150 |
+
REM Specify a particular script
|
151 |
+
run_in_docker.bat run_deepseek_example.py "your question"
|
152 |
+
```
|
153 |
+
|
154 |
+
**Available Scripts**:
|
155 |
+
• `run.py` - Default script, uses OpenAI GPT-4o model
|
156 |
+
• `run_deepseek_example.py` - Uses the DeepSeek model
|
157 |
+
• `run_gaia_roleplaying.py` - GAIA benchmark script
|
158 |
+
|
159 |
+
## Directory Mounts
|
160 |
+
|
161 |
+
The Docker Compose configuration has set up the following mount points:
|
162 |
+
|
163 |
+
• `./owl/.env:/app/owl/.env`: Mounts the environment variable file for easy modification of API keys
|
164 |
+
• `./data:/app/data`: Mounts the data directory for storing and accessing data files
|
165 |
+
• `playwright-cache`: Persistent volume for caching Playwright browsers
|
166 |
+
• `pip-cache`: Persistent volume for caching pip packages
|
167 |
+
|
168 |
+
## Environment Variables
|
169 |
+
|
170 |
+
You can set environment variables in two ways:
|
171 |
+
|
172 |
+
1. Modify the `owl/.env` file
|
173 |
+
2. Add environment variables in the `environment` section of the `docker-compose.yml` file
|
174 |
+
|
175 |
+
## Build Optimization
|
176 |
+
|
177 |
+
This Docker configuration includes several build optimizations:
|
178 |
+
|
179 |
+
1. **Use of Domestic Mirror Sources**: Uses Tsinghua University mirror sources to accelerate pip package downloads
|
180 |
+
2. **Layer Optimization**: Reduces the number of layers in the Dockerfile to improve build efficiency
|
181 |
+
3. **Cache Utilization**:
|
182 |
+
• Enables pip caching to avoid repeated dependency downloads
|
183 |
+
• Uses Docker BuildKit inline caching
|
184 |
+
• Arranges Dockerfile instructions to maximize cache utilization
|
185 |
+
4. **BuildKit**: Enables Docker BuildKit to accelerate builds
|
186 |
+
5. **Persistent Caching**:
|
187 |
+
• Uses Docker volumes to cache pip packages (`pip-cache`)
|
188 |
+
• Uses Docker volumes to cache Playwright browsers (`playwright-cache`)
|
189 |
+
• Local cache directory (`.docker-cache`)
|
190 |
+
|
191 |
+
### Cache Cleanup
|
192 |
+
|
193 |
+
If you need to clean the cache, you can use the following commands:
|
194 |
+
|
195 |
+
```bash
|
196 |
+
# Clean Docker build cache
|
197 |
+
docker builder prune
|
198 |
+
|
199 |
+
# Clean Docker volumes (will delete all unused volumes, including cache volumes)
|
200 |
+
docker volume prune
|
201 |
+
|
202 |
+
# Clean local cache directory
|
203 |
+
rm -rf .docker-cache
|
204 |
+
```
|
205 |
+
|
206 |
+
## Cross-Platform Compatibility
|
207 |
+
|
208 |
+
This project provides scripts for different operating systems:
|
209 |
+
|
210 |
+
1. **Check Scripts**:
|
211 |
+
• `check_docker.sh` (macOS/Linux): Checks the Docker environment
|
212 |
+
• `check_docker.bat` (Windows): Checks the Docker environment
|
213 |
+
|
214 |
+
2. **Build Scripts**:
|
215 |
+
• `build_docker.sh` (macOS/Linux): Builds the Docker image
|
216 |
+
• `build_docker.bat` (Windows): Builds the Docker image
|
217 |
+
|
218 |
+
3. **Run Scripts**:
|
219 |
+
• `run_in_docker.sh` (macOS/Linux): Runs scripts in the Docker container
|
220 |
+
• `run_in_docker.bat` (Windows): Runs scripts in the Docker container
|
221 |
+
|
222 |
+
These scripts automatically detect the operating system type and use appropriate commands.
|
223 |
+
|
224 |
+
## Troubleshooting
|
225 |
+
|
226 |
+
### Container Fails to Start
|
227 |
+
|
228 |
+
Check the logs for more information:
|
229 |
+
|
230 |
+
```bash
|
231 |
+
docker-compose logs
|
232 |
+
```
|
233 |
+
|
234 |
+
### API Key Issues
|
235 |
+
|
236 |
+
Ensure that you have correctly set all necessary API keys in the `owl/.env` file.
|
237 |
+
|
238 |
+
### Docker Compose Warnings
|
239 |
+
|
240 |
+
If you see a warning about the `version` attribute being obsolete:
|
241 |
+
|
242 |
+
```
|
243 |
+
WARN[0000] docker-compose.yml: the attribute `version` is obsolete
|
244 |
+
```
|
245 |
+
|
246 |
+
This is because you are using Docker Compose v2.x, which no longer requires an explicit version number. We have removed this attribute from the configuration file, so you should no longer see this warning.
|
247 |
+
|
248 |
+
### Browser-Related Issues
|
249 |
+
|
250 |
+
If you encounter browser-related issues, try the following solutions:
|
251 |
+
|
252 |
+
1. Ensure that you are using the `xvfb-python` command to run Python scripts in the Docker container
|
253 |
+
2. Check that Xvfb and related dependencies are correctly installed
|
254 |
+
3. Increase the shared memory size (set to 2GB in docker-compose.yml)
|
255 |
+
|
256 |
+
### Slow Build Speed
|
257 |
+
|
258 |
+
If the build speed is slow, try the following solutions:
|
259 |
+
|
260 |
+
1. Ensure that Docker BuildKit is enabled (`DOCKER_BUILDKIT=1`)
|
261 |
+
2. Ensure that pip caching is enabled (configured in docker-compose.yml)
|
262 |
+
3. Use the `--build-arg BUILDKIT_INLINE_CACHE=1` parameter when building (configured in the build script)
|
263 |
+
4. If this is the first build, downloading dependencies may take some time, but subsequent builds will be faster
|
264 |
+
|
265 |
+
### Windows-Specific Issues
|
266 |
+
|
267 |
+
If you encounter issues on Windows:
|
268 |
+
|
269 |
+
1. Ensure that you are running the Command Prompt or PowerShell with administrator privileges
|
270 |
+
2. If you encounter path issues, try using forward slashes (/) instead of backslashes (\)
|
271 |
+
3. If you encounter Docker Compose command issues, try using `docker compose` (without the hyphen)
|
272 |
+
|
273 |
+
### Insufficient Memory
|
274 |
+
|
275 |
+
If you encounter insufficient memory issues, you can adjust resource limits in the `docker-compose.yml` file:
|
276 |
+
|
277 |
+
```yaml
|
278 |
+
services:
|
279 |
+
owl:
|
280 |
+
# Other configurations...
|
281 |
+
deploy:
|
282 |
+
resources:
|
283 |
+
limits:
|
284 |
+
cpus: '4' # Increase CPU cores
|
285 |
+
memory: 8G # Increase memory limit
|
286 |
+
```
|
287 |
+
|
288 |
+
## Custom Docker Image
|
289 |
+
|
290 |
+
If you need to customize the Docker image, modify the `Dockerfile` file and then rebuild:
|
291 |
+
|
292 |
+
```bash
|
293 |
+
# macOS/Linux
|
294 |
+
./build_docker.sh
|
295 |
+
|
296 |
+
# Windows
|
297 |
+
build_docker.bat
|
298 |
+
```
|
README.md
ADDED
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<h1 align="center">
|
2 |
+
🦉 OWL: Optimized Workforce Learning for General Multi-Agent Assistance in Real-World Task Automation
|
3 |
+
</h1>
|
4 |
+
|
5 |
+
|
6 |
+
<div align="center">
|
7 |
+
|
8 |
+
[![Documentation][docs-image]][docs-url]
|
9 |
+
[![Discord][discord-image]][discord-url]
|
10 |
+
[![X][x-image]][x-url]
|
11 |
+
[![Reddit][reddit-image]][reddit-url]
|
12 |
+
[![Wechat][wechat-image]][wechat-url]
|
13 |
+
[![Wechat][owl-image]][owl-url]
|
14 |
+
[![Hugging Face][huggingface-image]][huggingface-url]
|
15 |
+
[![Star][star-image]][star-url]
|
16 |
+
[![Package License][package-license-image]][package-license-url]
|
17 |
+
|
18 |
+
|
19 |
+
</div>
|
20 |
+
|
21 |
+
|
22 |
+
<hr>
|
23 |
+
|
24 |
+
<div align="center">
|
25 |
+
<h4 align="center">
|
26 |
+
|
27 |
+
[中文阅读](https://github.com/camel-ai/owl/tree/main/README_zh.md) |
|
28 |
+
[Community](https://github.com/camel-ai/owl#community) |
|
29 |
+
[Installation](#️-installation) |
|
30 |
+
[Examples](https://github.com/camel-ai/owl/tree/main/owl) |
|
31 |
+
[Paper](https://arxiv.org/abs/2303.17760) |
|
32 |
+
[Citation](https://github.com/camel-ai/owl#citation) |
|
33 |
+
[Contributing](https://github.com/camel-ai/owl/graphs/contributors) |
|
34 |
+
[CAMEL-AI](https://www.camel-ai.org/)
|
35 |
+
|
36 |
+
</h4>
|
37 |
+
|
38 |
+
<div align="center" style="background-color: #f0f7ff; padding: 10px; border-radius: 5px; margin: 15px 0;">
|
39 |
+
<h3 style="color: #1e88e5; margin: 0;">
|
40 |
+
🏆 OWL achieves <span style="color: #d81b60; font-weight: bold; font-size: 1.2em;">58.18</span> average score on GAIA benchmark and ranks <span style="color: #d81b60; font-weight: bold; font-size: 1.2em;">🏅️ #1</span> among open-source frameworks! 🏆
|
41 |
+
</h3>
|
42 |
+
</div>
|
43 |
+
|
44 |
+
<div align="center">
|
45 |
+
|
46 |
+
🦉 OWL is a cutting-edge framework for multi-agent collaboration that pushes the boundaries of task automation, built on top of the [CAMEL-AI Framework](https://github.com/camel-ai/camel).
|
47 |
+
|
48 |
+
<!-- OWL achieves **58.18** average score on [GAIA](https://huggingface.co/spaces/gaia-benchmark/leaderboard) benchmark and ranks 🏅️ #1 among open-source frameworks. -->
|
49 |
+
|
50 |
+
Our vision is to revolutionize how AI agents collaborate to solve real-world tasks. By leveraging dynamic agent interactions, OWL enables more natural, efficient, and robust task automation across diverse domains.
|
51 |
+
|
52 |
+
</div>
|
53 |
+
|
54 |
+

|
55 |
+
|
56 |
+
<br>
|
57 |
+
|
58 |
+
|
59 |
+
</div>
|
60 |
+
|
61 |
+
<!-- # Key Features -->
|
62 |
+
# 📋 Table of Contents
|
63 |
+
|
64 |
+
- [📋 Table of Contents](#-table-of-contents)
|
65 |
+
- [🔥 News](#-news)
|
66 |
+
- [🎬 Demo Video](#-demo-video)
|
67 |
+
- [✨️ Core Features](#-core-features)
|
68 |
+
- [🛠️ Installation](#️-installation)
|
69 |
+
- [**Clone the Github repository**](#clone-the-github-repository)
|
70 |
+
- [**Set up Environment**](#set-up-environment)
|
71 |
+
- [**Install Dependencies**](#install-dependencies)
|
72 |
+
- [**Setup Environment Variables**](#setup-environment-variables)
|
73 |
+
- [**Running with Docker**](#running-with-docker)
|
74 |
+
|
75 |
+
- [🚀 Quick Start](#-quick-start)
|
76 |
+
- [🧪 Experiments](#-experiments)
|
77 |
+
- [⏱️ Future Plans](#️-future-plans)
|
78 |
+
- [📄 License](#-license)
|
79 |
+
- [🖊️ Cite](#️-cite)
|
80 |
+
- [🔥 Community](#-community)
|
81 |
+
- [❓ FAQ](#-faq)
|
82 |
+
- [⭐ Star History](#-star-history)
|
83 |
+
|
84 |
+
|
85 |
+
# 🔥 News
|
86 |
+
|
87 |
+
- **[2025.03.07]**: We open-source the codebase of 🦉 OWL project.
|
88 |
+
|
89 |
+
# 🎬 Demo Video
|
90 |
+
|
91 |
+
https://private-user-images.githubusercontent.com/55657767/420211368-f29f477d-7eef-46da-8d7a-8f3bcf506da2.mp4
|
92 |
+
|
93 |
+
https://private-user-images.githubusercontent.com/55657767/420212194-e813fc05-136a-485f-8df3-f10d9b4e63ec.mp4
|
94 |
+
|
95 |
+
# ✨️ Core Features
|
96 |
+
|
97 |
+
- **Real-time Information Retrieval**: Leverage Wikipedia, Google Search, and other online sources for up-to-date information.
|
98 |
+
- **Multimodal Processing**: Support for handling internet or local videos, images, and audio data.
|
99 |
+
- **Browser Automation**: Utilize the Playwright framework for simulating browser interactions, including scrolling, clicking, input handling, downloading, navigation, and more.
|
100 |
+
- **Document Parsing**: Extract content from Word, Excel, PDF, and PowerPoint files, converting them into text or Markdown format.
|
101 |
+
- **Code Execution**: Write and execute Python code using interpreter.
|
102 |
+
|
103 |
+
# 🛠️ Installation
|
104 |
+
|
105 |
+
## **Clone the Github repository**
|
106 |
+
|
107 |
+
```bash
|
108 |
+
git clone https://github.com/camel-ai/owl.git
|
109 |
+
cd owl
|
110 |
+
```
|
111 |
+
|
112 |
+
## **Set up Environment**
|
113 |
+
|
114 |
+
Using Conda (recommended):
|
115 |
+
```bash
|
116 |
+
conda create -n owl python=3.11
|
117 |
+
conda activate owl
|
118 |
+
```
|
119 |
+
|
120 |
+
Using venv (alternative):
|
121 |
+
```bash
|
122 |
+
python -m venv owl_env
|
123 |
+
# On Windows
|
124 |
+
owl_env\Scripts\activate
|
125 |
+
# On Unix or MacOS
|
126 |
+
source owl_env/bin/activate
|
127 |
+
```
|
128 |
+
|
129 |
+
|
130 |
+
## **Install Dependencies**
|
131 |
+
|
132 |
+
```bash
|
133 |
+
python -m pip install -r requirements.txt
|
134 |
+
playwright install
|
135 |
+
```
|
136 |
+
|
137 |
+
## **Setup Environment Variables**
|
138 |
+
|
139 |
+
In the `owl/.env_template` file, you will find all the necessary API keys along with the websites where you can register for each service. To use these API services, follow these steps:
|
140 |
+
|
141 |
+
1. *Copy and Rename*: Duplicate the `.env_example` file and rename the copy to `.env`.
|
142 |
+
```bash
|
143 |
+
cp owl/.env_template .env
|
144 |
+
```
|
145 |
+
2. *Fill in Your Keys*: Open the `.env` file and insert your API keys in the corresponding fields. (For the minimal example (`run_mini.py`), you only need to configure the LLM API key (e.g., OPENAI_API_KEY).)
|
146 |
+
3. *For using more other models*: please refer to our CAMEL models docs:https://docs.camel-ai.org/key_modules/models.html#supported-model-platforms-in-camel
|
147 |
+
|
148 |
+
|
149 |
+
> **Note**: For optimal performance, we strongly recommend using OpenAI models. Our experiments show that other models may result in significantly lower performance on complex tasks and benchmarks.
|
150 |
+
|
151 |
+
## **Running with Docker**
|
152 |
+
|
153 |
+
If you prefer to run the OWL project using Docker, we provide full Docker support:
|
154 |
+
|
155 |
+
```bash
|
156 |
+
# Clone the repository
|
157 |
+
git clone https://github.com/camel-ai/owl.git
|
158 |
+
cd owl
|
159 |
+
|
160 |
+
# Configure environment variables
|
161 |
+
cp owl/.env_template owl/.env
|
162 |
+
# Edit the .env file and fill in your API keys
|
163 |
+
|
164 |
+
# Build and run the Docker container
|
165 |
+
docker-compose up -d
|
166 |
+
|
167 |
+
# Run OWL inside the container
|
168 |
+
docker-compose exec owl bash -c "xvfb-python run.py"
|
169 |
+
```
|
170 |
+
|
171 |
+
For more detailed Docker usage instructions, including cross-platform support, optimized configurations, and troubleshooting, please refer to [DOCKER_README.md](DOCKER_README_en.md).
|
172 |
+
|
173 |
+
# 🚀 Quick Start
|
174 |
+
|
175 |
+
|
176 |
+
|
177 |
+
Run the following demo case:
|
178 |
+
|
179 |
+
```bash
|
180 |
+
python owl/run.py
|
181 |
+
```
|
182 |
+
|
183 |
+
## Running with Different Models
|
184 |
+
|
185 |
+
OWL supports various LLM backends. You can use the following scripts to run with different models:
|
186 |
+
|
187 |
+
```bash
|
188 |
+
# Run with Qwen model
|
189 |
+
python owl/run_qwen.py
|
190 |
+
|
191 |
+
# Run with Deepseek model
|
192 |
+
python owl/run_deepseek.py
|
193 |
+
|
194 |
+
# Run with other OpenAI-compatible models
|
195 |
+
python owl/run_openai_compatiable_model.py
|
196 |
+
```
|
197 |
+
|
198 |
+
For a simpler version that only requires an LLM API key, you can try our minimal example:
|
199 |
+
|
200 |
+
```bash
|
201 |
+
python owl/run_mini.py
|
202 |
+
```
|
203 |
+
|
204 |
+
You can run OWL agent with your own task by modifying the `run.py` script:
|
205 |
+
|
206 |
+
```python
|
207 |
+
# Define your own task
|
208 |
+
question = "Task description here."
|
209 |
+
|
210 |
+
society = construct_society(question)
|
211 |
+
answer, chat_history, token_count = run_society(society)
|
212 |
+
|
213 |
+
print(f"Answer: {answer}")
|
214 |
+
```
|
215 |
+
|
216 |
+
For uploading files, simply provide the file path along with your question:
|
217 |
+
|
218 |
+
```python
|
219 |
+
# Task with a local file (e.g., file path: `tmp/example.docx`)
|
220 |
+
question = "What is in the given DOCX file? Here is the file path: tmp/example.docx"
|
221 |
+
|
222 |
+
society = construct_society(question)
|
223 |
+
answer, chat_history, token_count = run_society(society)
|
224 |
+
print(f"Answer: {answer}")
|
225 |
+
```
|
226 |
+
|
227 |
+
OWL will then automatically invoke document-related tools to process the file and extract the answer.
|
228 |
+
|
229 |
+
|
230 |
+
Example tasks you can try:
|
231 |
+
- "Find the latest stock price for Apple Inc."
|
232 |
+
- "Analyze the sentiment of recent tweets about climate change"
|
233 |
+
- "Help me debug this Python code: [your code here]"
|
234 |
+
- "Summarize the main points from this research paper: [paper URL]"
|
235 |
+
|
236 |
+
# 🧪 Experiments
|
237 |
+
|
238 |
+
We provided a script to reproduce the results on GAIA.
|
239 |
+
You can check the `run_gaia_roleplaying.py` file and run the following command:
|
240 |
+
|
241 |
+
```bash
|
242 |
+
python run_gaia_roleplaying.py
|
243 |
+
```
|
244 |
+
|
245 |
+
# ⏱️ Future Plans
|
246 |
+
|
247 |
+
- [ ] Write a technical blog post detailing our exploration and insights in multi-agent collaboration in real-world tasks.
|
248 |
+
- [ ] Enhance the toolkit ecosystem with more specialized tools for domain-specific tasks.
|
249 |
+
- [ ] Develop more sophisticated agent interaction patterns and communication protocols
|
250 |
+
|
251 |
+
|
252 |
+
# 📄 License
|
253 |
+
|
254 |
+
The source code is licensed under Apache 2.0.
|
255 |
+
|
256 |
+
# 🖊️ Cite
|
257 |
+
|
258 |
+
If you find this repo useful, please cite:
|
259 |
+
|
260 |
+
|
261 |
+
```
|
262 |
+
@misc{owl2025,
|
263 |
+
title = {OWL: Optimized Workforce Learning for General Multi-Agent Assistance in Real-World Task Automation},
|
264 |
+
author = {{CAMEL-AI.org}},
|
265 |
+
howpublished = {\url{https://github.com/camel-ai/owl}},
|
266 |
+
note = {Accessed: 2025-03-07},
|
267 |
+
year = {2025}
|
268 |
+
}
|
269 |
+
```
|
270 |
+
|
271 |
+
# 🔥 Community
|
272 |
+
Join us for further discussions!
|
273 |
+
<!--  -->
|
274 |
+

|
275 |
+
<!--  -->
|
276 |
+
|
277 |
+
# ❓ FAQ
|
278 |
+
|
279 |
+
**Q: Why don't I see Chrome running locally after starting the example script?**
|
280 |
+
|
281 |
+
A: If OWL determines that a task can be completed using non-browser tools (such as search or code execution), the browser will not be launched. The browser window will only appear when OWL determines that browser-based interaction is necessary.
|
282 |
+
|
283 |
+
# ⭐ Star History
|
284 |
+
|
285 |
+
[](https://star-history.com/#camel-ai/owl&Date)
|
286 |
+
|
287 |
+
|
288 |
+
|
289 |
+
[docs-image]: https://img.shields.io/badge/Documentation-EB3ECC
|
290 |
+
[docs-url]: https://camel-ai.github.io/camel/index.html
|
291 |
+
[star-image]: https://img.shields.io/github/stars/camel-ai/owl?label=stars&logo=github&color=brightgreen
|
292 |
+
[star-url]: https://github.com/camel-ai/owl/stargazers
|
293 |
+
[package-license-image]: https://img.shields.io/badge/License-Apache_2.0-blue.svg
|
294 |
+
[package-license-url]: https://github.com/camel-ai/owl/blob/main/licenses/LICENSE
|
295 |
+
|
296 |
+
[colab-url]: https://colab.research.google.com/drive/1AzP33O8rnMW__7ocWJhVBXjKziJXPtim?usp=sharing
|
297 |
+
[colab-image]: https://colab.research.google.com/assets/colab-badge.svg
|
298 |
+
[huggingface-url]: https://huggingface.co/camel-ai
|
299 |
+
[huggingface-image]: https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-CAMEL--AI-ffc107?color=ffc107&logoColor=white
|
300 |
+
[discord-url]: https://discord.camel-ai.org/
|
301 |
+
[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb
|
302 |
+
[wechat-url]: https://ghli.org/camel/wechat.png
|
303 |
+
[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white
|
304 |
+
[x-url]: https://x.com/CamelAIOrg
|
305 |
+
[x-image]: https://img.shields.io/twitter/follow/CamelAIOrg?style=social
|
306 |
+
[twitter-image]: https://img.shields.io/twitter/follow/CamelAIOrg?style=social&color=brightgreen&logo=twitter
|
307 |
+
[reddit-url]: https://www.reddit.com/r/CamelAI/
|
308 |
+
[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white
|
309 |
+
[ambassador-url]: https://www.camel-ai.org/community
|
310 |
+
[owl-url]: ./assets/qr_code.jpg
|
311 |
+
[owl-image]: https://img.shields.io/badge/WeChat-OWLProject-brightgreen?logo=wechat&logoColor=white
|
README_zh.md
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<h1 align="center">
|
2 |
+
🦉 OWL: Optimized Workforce Learning for General Multi-Agent Assistance in Real-World Task Automation
|
3 |
+
🦉 OWL: 优化劳动力学习的通用智能体,用于处理现实世界的自动化任务
|
4 |
+
</h1>
|
5 |
+
|
6 |
+
|
7 |
+
<div align="center">
|
8 |
+
|
9 |
+
[![文档][docs-image]][docs-url]
|
10 |
+
[![Discord][discord-image]][discord-url]
|
11 |
+
[![X][x-image]][x-url]
|
12 |
+
[![Reddit][reddit-image]][reddit-url]
|
13 |
+
[![微信][wechat-image]][wechat-url]
|
14 |
+
[![微信][owl-image]][owl-url]
|
15 |
+
[![Hugging Face][huggingface-image]][huggingface-url]
|
16 |
+
[![Star][star-image]][star-url]
|
17 |
+
[![软件许可证][package-license-image]][package-license-url]
|
18 |
+
|
19 |
+
|
20 |
+
</div>
|
21 |
+
|
22 |
+
|
23 |
+
<hr>
|
24 |
+
|
25 |
+
<div align="center">
|
26 |
+
<h4 align="center">
|
27 |
+
|
28 |
+
[English README](https://github.com/camel-ai/owl/tree/main) |
|
29 |
+
[社区](https://github.com/camel-ai/camel#community) |
|
30 |
+
[安装](#️-installation) |
|
31 |
+
[示例](https://github.com/camel-ai/owl/tree/main/owl) |
|
32 |
+
[论文](https://arxiv.org/abs/2303.17760) |
|
33 |
+
[引用](#-community) |
|
34 |
+
[贡献](https://github.com/camel-ai/owl/graphs/contributors) |
|
35 |
+
[CAMEL-AI](https://www.camel-ai.org/)
|
36 |
+
|
37 |
+
</h4>
|
38 |
+
|
39 |
+
<div align="center" style="background-color: #f0f7ff; padding: 10px; border-radius: 5px; margin: 15px 0;">
|
40 |
+
<h3 style="color: #1e88e5; margin: 0;">
|
41 |
+
🏆 OWL 在 GAIA 基准测试中取得 <span style="color: #d81b60; font-weight: bold; font-size: 1.2em;">58.18</span> 平均分,在开源框架中排名 <span style="color: #d81b60; font-weight: bold; font-size: 1.2em;">🏅️ #1</span>! 🏆
|
42 |
+
</h3>
|
43 |
+
</div>
|
44 |
+
|
45 |
+
<div align="center">
|
46 |
+
|
47 |
+
🦉 OWL 是一个前沿的多智能体协作框架,推动任务自动化的边界,构建在 [CAMEL-AI Framework](https://github.com/camel-ai/camel)。
|
48 |
+
|
49 |
+
我们的愿景是彻底变革 AI 智能体协作解决现实任务的方式。通过利用动态智能体交互,OWL 实现了跨多领域更自然、高效且稳健的任务自动化。
|
50 |
+
|
51 |
+
</div>
|
52 |
+
|
53 |
+

|
54 |
+
|
55 |
+
|
56 |
+
|
57 |
+
<br>
|
58 |
+
|
59 |
+
|
60 |
+
</div>
|
61 |
+
|
62 |
+
<!-- # Key Features -->
|
63 |
+
# 📋 目录
|
64 |
+
|
65 |
+
- [📋 目录](#-目录)
|
66 |
+
- [🔥 新闻](#-新闻)
|
67 |
+
- [🎬 演示视频](#-演示视频)
|
68 |
+
- [✨️ 核心功能](#-核心功能)
|
69 |
+
- [🛠️ 安装](#️-安装)
|
70 |
+
- [**克隆 Github 仓库**](#克隆-github-仓库)
|
71 |
+
- [**设置环境**](#设置环境)
|
72 |
+
- [**安装依赖**](#安装依赖)
|
73 |
+
- [**设置环境变量**](#设置环境变量)
|
74 |
+
- [**使用Docker运行**](#使用docker运行)
|
75 |
+
- [🚀 快速开始](#-快速开始)
|
76 |
+
- [🧪 实验](#-实验)
|
77 |
+
- [⏱️ 未来计划](#️-未来计划)
|
78 |
+
- [📄 许可证](#-许可证)
|
79 |
+
- [🖊️ 引用](#️-引用)
|
80 |
+
- [🔥 社区](#-社区)
|
81 |
+
- [❓ 常见问题](#-常见问题)
|
82 |
+
|
83 |
+
|
84 |
+
# 🔥 新闻
|
85 |
+
|
86 |
+
- **[2025.03.07]**: 我们开源了 🦉 OWL 项目的代码库。
|
87 |
+
|
88 |
+
# 🎬 演示视频
|
89 |
+
|
90 |
+
https://private-user-images.githubusercontent.com/55657767/420211368-f29f477d-7eef-46da-8d7a-8f3bcf506da2.mp4
|
91 |
+
|
92 |
+
https://private-user-images.githubusercontent.com/55657767/420212194-e813fc05-136a-485f-8df3-f10d9b4e63ec.mp4
|
93 |
+
|
94 |
+
# ✨️ 核心功能
|
95 |
+
|
96 |
+
- **在线搜索**:使用维基百科、谷歌搜索等,进行实时信息检索
|
97 |
+
- **多模态处理**:支持互联网或本地视频、图片、语音处理
|
98 |
+
- **浏览器操作**:借助Playwright框架开发浏览器模拟交互,支持页面滚动、点击、输入、下载、历史回退等功能
|
99 |
+
- **文件解析**:word、excel、PDF、PowerPoint信息提取,内容转文本/Markdown
|
100 |
+
- **代码执行**:编写python代码,并使用解释器运行
|
101 |
+
|
102 |
+
# 🛠️ 安装
|
103 |
+
|
104 |
+
## **克隆 Github 仓库**
|
105 |
+
|
106 |
+
```bash
|
107 |
+
git clone https://github.com/camel-ai/owl.git
|
108 |
+
cd owl
|
109 |
+
```
|
110 |
+
|
111 |
+
## **设置环境**
|
112 |
+
|
113 |
+
使用 Conda(推荐):
|
114 |
+
```bash
|
115 |
+
conda create -n owl python=3.11
|
116 |
+
conda activate owl
|
117 |
+
```
|
118 |
+
|
119 |
+
使用 venv(备用):
|
120 |
+
```bash
|
121 |
+
python -m venv owl_env
|
122 |
+
# Windows 系统
|
123 |
+
owl_env\Scripts\activate
|
124 |
+
# Unix 或 MacOS 系统
|
125 |
+
source owl_env/bin/activate
|
126 |
+
```
|
127 |
+
|
128 |
+
## **安装依赖**
|
129 |
+
|
130 |
+
```bash
|
131 |
+
python -m pip install -r requirements.txt
|
132 |
+
```
|
133 |
+
|
134 |
+
## **设置环境变量**
|
135 |
+
|
136 |
+
在 `owl/.env_template` 文件中,你可以找到所有必要的 API 密钥以及各服务的注册网址。要使用这些 API 服务,请按照以下步骤操作:
|
137 |
+
|
138 |
+
1. *复制并重命名*: 复制 `.env_example` 文件,并将副本重命名为 `.env`。
|
139 |
+
2. *填写你的密钥*: 打开 `.env` 文件,在相应字段中填入你的 API 密钥。
|
140 |
+
3. *如需使用更多其他模型*:请参考我们CAMEL的models文档:https://docs.camel-ai.org/key_modules/models.html#supported-model-platforms-in-camel
|
141 |
+
|
142 |
+
> **注意**:为获得最佳性能,我们强烈建议使用 OpenAI 模型。我们通过测试发现,其他模型在处理复杂任务和基准测试时可能会导致性能显著降低。
|
143 |
+
|
144 |
+
## **使用Docker运行**
|
145 |
+
|
146 |
+
如果您希望使用Docker运行OWL项目,我们提供了完整的Docker支持:
|
147 |
+
|
148 |
+
```bash
|
149 |
+
# 克隆仓库
|
150 |
+
git clone https://github.com/camel-ai/owl.git
|
151 |
+
cd owl
|
152 |
+
|
153 |
+
# 配置环境变量
|
154 |
+
cp owl/.env_template owl/.env
|
155 |
+
# 编辑.env文件,填入您的API密钥
|
156 |
+
|
157 |
+
# 构建并运行Docker容器
|
158 |
+
docker-compose up -d
|
159 |
+
|
160 |
+
# 在容器中���行OWL
|
161 |
+
docker-compose exec owl bash -c "xvfb-python run.py"
|
162 |
+
```
|
163 |
+
|
164 |
+
更多详细的Docker使用说明,包括跨平台支持、优化配置和故障排除,请参阅 [DOCKER_README.md](DOCKER_README.md)
|
165 |
+
|
166 |
+
# 🚀 快速开始
|
167 |
+
|
168 |
+
运行以下示例:
|
169 |
+
|
170 |
+
```bash
|
171 |
+
python owl/run.py
|
172 |
+
```
|
173 |
+
|
174 |
+
我们还提供了一个最小化示例,只需配置LLM的API密钥即可运行:
|
175 |
+
|
176 |
+
```bash
|
177 |
+
python owl/run_mini.py
|
178 |
+
```
|
179 |
+
|
180 |
+
## 使用不同的模型
|
181 |
+
|
182 |
+
OWL 支持多种 LLM 后端。您可以使用以下脚本来运行不同的模型:
|
183 |
+
|
184 |
+
```bash
|
185 |
+
# 使用 Qwen 模型运行
|
186 |
+
python owl/run_qwen.py
|
187 |
+
|
188 |
+
# 使用 Deepseek 模型运行
|
189 |
+
python owl/run_deepseek.py
|
190 |
+
|
191 |
+
# 使用其他 OpenAI 兼容模型运行
|
192 |
+
python owl/run_openai_compatiable_model.py
|
193 |
+
```
|
194 |
+
|
195 |
+
你可以通过修改 `run.py` 脚本来运行自己的任务:
|
196 |
+
|
197 |
+
```python
|
198 |
+
# Define your own task
|
199 |
+
question = "Task description here."
|
200 |
+
|
201 |
+
society = construct_society(question)
|
202 |
+
answer, chat_history, token_count = run_society(society)
|
203 |
+
|
204 |
+
print(f"Answer: {answer}")
|
205 |
+
```
|
206 |
+
|
207 |
+
上传文件时,只需提供文件路径和问题:
|
208 |
+
|
209 |
+
```python
|
210 |
+
# 处理本地文件(例如,文件路径为 `tmp/example.docx`)
|
211 |
+
question = "给定的 DOCX 文件中有什么内容?文件路径如下:tmp/example.docx"
|
212 |
+
|
213 |
+
society = construct_society(question)
|
214 |
+
answer, chat_history, token_count = run_society(society)
|
215 |
+
|
216 |
+
print(f"答案:{answer}")
|
217 |
+
```
|
218 |
+
|
219 |
+
OWL 将自动调用与文档相关的工具来处理文件并提取答案。
|
220 |
+
|
221 |
+
你可以尝试以下示例任务:
|
222 |
+
- "查询苹果公司的最新股票价格"
|
223 |
+
- "分析关于气候变化的最新推文情绪"
|
224 |
+
- "帮我调试这段 Python 代码:[在此粘贴你的代码]"
|
225 |
+
- "总结这篇研究论文的主要观点:[论文URL]"
|
226 |
+
|
227 |
+
# 🧪 实验
|
228 |
+
|
229 |
+
我们提供了一个脚本用于复现 GAIA 上的实验结果。
|
230 |
+
你可以查看 `run_gaia_roleplaying.py` 文件,并运行以下命令:
|
231 |
+
|
232 |
+
```bash
|
233 |
+
python run_gaia_roleplaying.py
|
234 |
+
```
|
235 |
+
|
236 |
+
# ⏱️ 未来计划
|
237 |
+
|
238 |
+
- [ ] 撰写一篇技术博客,详细介绍我们在现实任务中多智能体协作方面的探索与见解。
|
239 |
+
- [ ] 通过引入更多针对特定领域任务的专业工具,进一步完善工具生态系统。
|
240 |
+
- [ ] 开发更复杂的智能体交互模式和通信协议
|
241 |
+
|
242 |
+
|
243 |
+
# 📄 许可证
|
244 |
+
|
245 |
+
源代码采用 Apache 2.0 许可证。
|
246 |
+
|
247 |
+
# 🖊️ 引用
|
248 |
+
|
249 |
+
如果你觉得这个仓库对你有帮助,请引用:
|
250 |
+
|
251 |
+
|
252 |
+
```
|
253 |
+
@misc{owl2025,
|
254 |
+
title = {OWL: Optimized Workforce Learning for General Multi-Agent Assistance in Real-World Task Automation},
|
255 |
+
author = {{CAMEL-AI.org}},
|
256 |
+
howpublished = {\url{https://github.com/camel-ai/owl}},
|
257 |
+
note = {Accessed: 2025-03-07},
|
258 |
+
year = {2025}
|
259 |
+
}
|
260 |
+
```
|
261 |
+
|
262 |
+
# 🔥 社区
|
263 |
+
加入我们,参与更多讨论!
|
264 |
+
<!--  -->
|
265 |
+

|
266 |
+
<!--  -->
|
267 |
+
|
268 |
+
# ❓ 常见问题
|
269 |
+
|
270 |
+
**Q: 为什么启动示例脚本后,我没有看到本地运行Chrome浏览器?**
|
271 |
+
|
272 |
+
A: 当OWL判断某个任务可以使用非浏览器工具(如搜索、代码分析等)完成时,浏览器就不会启动。只有在判断需要使用浏览器工具的时候,本地才会弹出浏览器窗口,并进行浏览器模拟交互。
|
273 |
+
|
274 |
+
[docs-image]: https://img.shields.io/badge/Documentation-EB3ECC
|
275 |
+
[docs-url]: https://camel-ai.github.io/camel/index.html
|
276 |
+
[star-image]: https://img.shields.io/github/stars/camel-ai/owl?label=stars&logo=github&color=brightgreen
|
277 |
+
[star-url]: https://github.com/camel-ai/owl/stargazers
|
278 |
+
[package-license-image]: https://img.shields.io/badge/License-Apache_2.0-blue.svg
|
279 |
+
[package-license-url]: https://github.com/camel-ai/owl/blob/main/licenses/LICENSE
|
280 |
+
|
281 |
+
[colab-url]: https://colab.research.google.com/drive/1AzP33O8rnMW__7ocWJhVBXjKziJXPtim?usp=sharing
|
282 |
+
[colab-image]: https://colab.research.google.com/assets/colab-badge.svg
|
283 |
+
[huggingface-url]: https://huggingface.co/camel-ai
|
284 |
+
[huggingface-image]: https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-CAMEL--AI-ffc107?color=ffc107&logoColor=white
|
285 |
+
[discord-url]: https://discord.camel-ai.org/
|
286 |
+
[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb
|
287 |
+
[wechat-url]: https://ghli.org/camel/wechat.png
|
288 |
+
[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white
|
289 |
+
[x-url]: https://x.com/CamelAIOrg
|
290 |
+
[x-image]: https://img.shields.io/twitter/follow/CamelAIOrg?style=social
|
291 |
+
[twitter-image]: https://img.shields.io/twitter/follow/CamelAIOrg?style=social&color=brightgreen&logo=twitter
|
292 |
+
[reddit-url]: https://www.reddit.com/r/CamelAI/
|
293 |
+
[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white
|
294 |
+
[ambassador-url]: https://www.camel-ai.org/community
|
295 |
+
[owl-url]: ./assets/qr_code.jpg
|
296 |
+
[owl-image]: https://img.shields.io/badge/WeChat-OWLProject-brightgreen?logo=wechat&logoColor=white
|
assets/community.png
ADDED
![]() |
Git LFS Details
|
assets/community_2.png
ADDED
![]() |
Git LFS Details
|
assets/community_3.jpg
ADDED
![]() |
Git LFS Details
|
assets/community_4.jpg
ADDED
![]() |
Git LFS Details
|
assets/community_5.jpg
ADDED
![]() |
Git LFS Details
|
assets/meetup.jpg
ADDED
![]() |
Git LFS Details
|
assets/owl_architecture.png
ADDED
![]() |
Git LFS Details
|
assets/qr_code.jpg
ADDED
![]() |
Git LFS Details
|
licenses/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. =========
|
licenses/license_template.txt
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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. =========
|
licenses/update_license.py
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 os
|
15 |
+
import re
|
16 |
+
import sys
|
17 |
+
from pathlib import Path
|
18 |
+
from typing import List
|
19 |
+
|
20 |
+
|
21 |
+
# The license template file is hard-coded with specific start and end lines
|
22 |
+
def fine_license_start_line(lines: List[str], start_with: str) -> int:
|
23 |
+
for i in range(len(lines)):
|
24 |
+
if lines[i].startswith(start_with):
|
25 |
+
return i
|
26 |
+
return None
|
27 |
+
|
28 |
+
|
29 |
+
def find_license_end_line(lines: List[str], start_with: str) -> int:
|
30 |
+
for i in range(len(lines) - 1, -1, -1):
|
31 |
+
if lines[i].startswith(start_with):
|
32 |
+
return i
|
33 |
+
return None
|
34 |
+
|
35 |
+
|
36 |
+
def update_license_in_file(
|
37 |
+
file_path: str,
|
38 |
+
license_template_path: str,
|
39 |
+
start_line_start_with: str,
|
40 |
+
end_line_start_with: str,
|
41 |
+
) -> bool:
|
42 |
+
with open(
|
43 |
+
file_path, 'r', encoding='utf-8'
|
44 |
+
) as f: # for windows compatibility
|
45 |
+
content = f.read()
|
46 |
+
|
47 |
+
with open(license_template_path, 'r', encoding='utf-8') as f:
|
48 |
+
new_license = f.read().strip()
|
49 |
+
|
50 |
+
maybe_existing_licenses = re.findall(
|
51 |
+
r'^#.*?(?=\n)', content, re.MULTILINE | re.DOTALL
|
52 |
+
)
|
53 |
+
start_index = fine_license_start_line(
|
54 |
+
maybe_existing_licenses, start_line_start_with
|
55 |
+
)
|
56 |
+
end_index = find_license_end_line(
|
57 |
+
maybe_existing_licenses, end_line_start_with
|
58 |
+
)
|
59 |
+
if start_index is not None and end_index is not None:
|
60 |
+
maybe_existing_licenses = maybe_existing_licenses[
|
61 |
+
start_index : end_index + 1
|
62 |
+
]
|
63 |
+
else:
|
64 |
+
maybe_existing_licenses = None
|
65 |
+
if maybe_existing_licenses:
|
66 |
+
maybe_old_licenses = '\n'.join(maybe_existing_licenses)
|
67 |
+
if maybe_old_licenses.strip() != new_license.strip():
|
68 |
+
replaced_content = content.replace(maybe_old_licenses, new_license)
|
69 |
+
with open(file_path, 'w') as f:
|
70 |
+
f.write(replaced_content)
|
71 |
+
print(f'Replaced license in {file_path}')
|
72 |
+
return True
|
73 |
+
else:
|
74 |
+
return False
|
75 |
+
else:
|
76 |
+
with open(file_path, 'w') as f:
|
77 |
+
f.write(new_license + '\n' + content)
|
78 |
+
print(f'Added license to {file_path}')
|
79 |
+
return True
|
80 |
+
|
81 |
+
|
82 |
+
def update_license_in_directory(
|
83 |
+
directory_path: str,
|
84 |
+
license_template_path: str,
|
85 |
+
start_line_start_with: str,
|
86 |
+
end_line_start_with: str,
|
87 |
+
) -> None:
|
88 |
+
# Check if directory exists
|
89 |
+
if not os.path.isdir(directory_path):
|
90 |
+
raise NotADirectoryError(f'{directory_path} is not a directory')
|
91 |
+
# Check if license template exists
|
92 |
+
if not os.path.isfile(license_template_path):
|
93 |
+
raise FileNotFoundError(f'{license_template_path} not found')
|
94 |
+
|
95 |
+
file_count = 0
|
96 |
+
for py_files in Path(directory_path).rglob("*.py"):
|
97 |
+
if py_files.name.startswith('.'):
|
98 |
+
continue
|
99 |
+
if any(part.startswith('.') for part in py_files.parts):
|
100 |
+
continue
|
101 |
+
if update_license_in_file(
|
102 |
+
py_files,
|
103 |
+
license_template_path,
|
104 |
+
start_line_start_with,
|
105 |
+
end_line_start_with,
|
106 |
+
):
|
107 |
+
file_count += 1
|
108 |
+
|
109 |
+
print(f'License updated in {file_count} files')
|
110 |
+
|
111 |
+
|
112 |
+
if __name__ == '__main__':
|
113 |
+
if len(sys.argv) < 3:
|
114 |
+
print(
|
115 |
+
"Usage from command line: "
|
116 |
+
"python update_license.py <directory_path> <license_template_path>"
|
117 |
+
"No valid input arguments found, please enter manually."
|
118 |
+
)
|
119 |
+
directory_path = input("Enter directory path: ")
|
120 |
+
license_template_path = input("Enter license template path: ")
|
121 |
+
else:
|
122 |
+
directory_path = sys.argv[1]
|
123 |
+
license_template_path = sys.argv[2]
|
124 |
+
|
125 |
+
start_line_start_with = "# ========= Copyright"
|
126 |
+
end_line_start_with = "# ========= Copyright"
|
127 |
+
update_license_in_directory(
|
128 |
+
directory_path,
|
129 |
+
license_template_path,
|
130 |
+
start_line_start_with,
|
131 |
+
end_line_start_with,
|
132 |
+
)
|
owl/.env_template
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# MODEL & API (See https://github.com/camel-ai/camel/blob/master/camel/types/enums.py)
|
2 |
+
|
3 |
+
# OPENAI API
|
4 |
+
OPENAI_API_KEY = ""
|
5 |
+
# OPENAI_API_BASE_URL = ""
|
6 |
+
|
7 |
+
# Qwen API (https://help.aliyun.com/zh/model-studio/developer-reference/get-api-key)
|
8 |
+
# QWEN_API_KEY=""
|
9 |
+
|
10 |
+
# DeepSeek API (https://platform.deepseek.com/api_keys)
|
11 |
+
# DEEPSEEK_API_KEY=""
|
12 |
+
|
13 |
+
#===========================================
|
14 |
+
# Tools & Services API
|
15 |
+
#===========================================
|
16 |
+
|
17 |
+
# Google Search API (https://developers.google.com/custom-search/v1/overview)
|
18 |
+
GOOGLE_API_KEY=""
|
19 |
+
SEARCH_ENGINE_ID=""
|
20 |
+
|
21 |
+
# Hugging Face API (https://huggingface.co/join)
|
22 |
+
HF_TOKEN=""
|
23 |
+
|
24 |
+
# Chunkr API (https://chunkr.ai/)
|
25 |
+
CHUNKR_API_KEY=""
|
26 |
+
|
27 |
+
# Firecrawl API (https://www.firecrawl.dev/)
|
28 |
+
FIRECRAWL_API_KEY=""
|
owl/camel/__init__.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from camel.logger import disable_logging, enable_logging, set_log_level
|
16 |
+
|
17 |
+
__version__ = '0.2.11'
|
18 |
+
|
19 |
+
__all__ = [
|
20 |
+
'__version__',
|
21 |
+
'camel',
|
22 |
+
'disable_logging',
|
23 |
+
'enable_logging',
|
24 |
+
'set_log_level',
|
25 |
+
]
|
owl/camel/agents/__init__.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .base import BaseAgent
|
15 |
+
from .chat_agent import ChatAgent
|
16 |
+
from .critic_agent import CriticAgent
|
17 |
+
from .embodied_agent import EmbodiedAgent
|
18 |
+
from .knowledge_graph_agent import KnowledgeGraphAgent
|
19 |
+
from .role_assignment_agent import RoleAssignmentAgent
|
20 |
+
from .search_agent import SearchAgent
|
21 |
+
from .task_agent import (
|
22 |
+
TaskCreationAgent,
|
23 |
+
TaskPlannerAgent,
|
24 |
+
TaskPrioritizationAgent,
|
25 |
+
TaskSpecifyAgent,
|
26 |
+
)
|
27 |
+
from .tool_agents.base import BaseToolAgent
|
28 |
+
from .tool_agents.hugging_face_tool_agent import HuggingFaceToolAgent
|
29 |
+
|
30 |
+
__all__ = [
|
31 |
+
'BaseAgent',
|
32 |
+
'ChatAgent',
|
33 |
+
'TaskSpecifyAgent',
|
34 |
+
'TaskPlannerAgent',
|
35 |
+
'TaskCreationAgent',
|
36 |
+
'TaskPrioritizationAgent',
|
37 |
+
'CriticAgent',
|
38 |
+
'BaseToolAgent',
|
39 |
+
'HuggingFaceToolAgent',
|
40 |
+
'EmbodiedAgent',
|
41 |
+
'RoleAssignmentAgent',
|
42 |
+
'SearchAgent',
|
43 |
+
'KnowledgeGraphAgent',
|
44 |
+
]
|
owl/camel/agents/base.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from abc import ABC, abstractmethod
|
15 |
+
from typing import Any
|
16 |
+
|
17 |
+
|
18 |
+
class BaseAgent(ABC):
|
19 |
+
r"""An abstract base class for all CAMEL agents."""
|
20 |
+
|
21 |
+
@abstractmethod
|
22 |
+
def reset(self, *args: Any, **kwargs: Any) -> Any:
|
23 |
+
r"""Resets the agent to its initial state."""
|
24 |
+
pass
|
25 |
+
|
26 |
+
@abstractmethod
|
27 |
+
def step(self, *args: Any, **kwargs: Any) -> Any:
|
28 |
+
r"""Performs a single step of the agent."""
|
29 |
+
pass
|
owl/camel/agents/chat_agent.py
ADDED
@@ -0,0 +1,1411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from __future__ import annotations
|
15 |
+
|
16 |
+
import json
|
17 |
+
# import logging
|
18 |
+
import re
|
19 |
+
import uuid
|
20 |
+
from collections import defaultdict
|
21 |
+
from typing import (
|
22 |
+
TYPE_CHECKING,
|
23 |
+
Any,
|
24 |
+
Callable,
|
25 |
+
Dict,
|
26 |
+
List,
|
27 |
+
Optional,
|
28 |
+
Tuple,
|
29 |
+
Type,
|
30 |
+
Union,
|
31 |
+
)
|
32 |
+
|
33 |
+
from loguru import logger
|
34 |
+
|
35 |
+
from openai.types.chat import ChatCompletionMessageToolCall
|
36 |
+
from openai.types.chat.chat_completion_message_tool_call import Function
|
37 |
+
from pydantic import BaseModel
|
38 |
+
|
39 |
+
from camel.agents.base import BaseAgent
|
40 |
+
from camel.memories import (
|
41 |
+
AgentMemory,
|
42 |
+
ChatHistoryMemory,
|
43 |
+
MemoryRecord,
|
44 |
+
ScoreBasedContextCreator,
|
45 |
+
)
|
46 |
+
from camel.messages import BaseMessage, FunctionCallingMessage, OpenAIMessage
|
47 |
+
from camel.models import (
|
48 |
+
BaseModelBackend,
|
49 |
+
ModelFactory,
|
50 |
+
ModelManager,
|
51 |
+
ModelProcessingError,
|
52 |
+
)
|
53 |
+
from camel.responses import ChatAgentResponse
|
54 |
+
from camel.types import (
|
55 |
+
ChatCompletion,
|
56 |
+
ChatCompletionChunk,
|
57 |
+
ModelPlatformType,
|
58 |
+
ModelType,
|
59 |
+
OpenAIBackendRole,
|
60 |
+
RoleType,
|
61 |
+
)
|
62 |
+
from camel.utils import (
|
63 |
+
func_string_to_callable,
|
64 |
+
get_model_encoding,
|
65 |
+
get_pydantic_object_schema,
|
66 |
+
json_to_function_code,
|
67 |
+
)
|
68 |
+
|
69 |
+
if TYPE_CHECKING:
|
70 |
+
from openai import Stream
|
71 |
+
|
72 |
+
from camel.terminators import ResponseTerminator
|
73 |
+
from camel.toolkits import FunctionTool
|
74 |
+
|
75 |
+
|
76 |
+
# logger = logging.getLogger(__name__)
|
77 |
+
|
78 |
+
# AgentOps decorator setting
|
79 |
+
try:
|
80 |
+
import os
|
81 |
+
|
82 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
83 |
+
from agentops import track_agent
|
84 |
+
else:
|
85 |
+
raise ImportError
|
86 |
+
except (ImportError, AttributeError):
|
87 |
+
from camel.utils import track_agent
|
88 |
+
|
89 |
+
|
90 |
+
class FunctionCallingRecord(BaseModel):
|
91 |
+
r"""Historical records of functions called in the conversation.
|
92 |
+
|
93 |
+
Attributes:
|
94 |
+
func_name (str): The name of the function being called.
|
95 |
+
args (Dict[str, Any]): The dictionary of arguments passed to
|
96 |
+
the function.
|
97 |
+
result (Any): The execution result of calling this function.
|
98 |
+
"""
|
99 |
+
|
100 |
+
func_name: str
|
101 |
+
args: Dict[str, Any]
|
102 |
+
result: Any
|
103 |
+
|
104 |
+
def __str__(self) -> str:
|
105 |
+
r"""Overridden version of the string function.
|
106 |
+
|
107 |
+
Returns:
|
108 |
+
str: Modified string to represent the function calling.
|
109 |
+
"""
|
110 |
+
return (
|
111 |
+
f"Function Execution: {self.func_name}\n"
|
112 |
+
f"\tArgs: {self.args}\n"
|
113 |
+
f"\tResult: {self.result}"
|
114 |
+
)
|
115 |
+
|
116 |
+
def as_dict(self) -> dict[str, Any]:
|
117 |
+
r"""Returns the function calling record as a dictionary.
|
118 |
+
|
119 |
+
Returns:
|
120 |
+
dict[str, Any]: The function calling record as a dictionary.
|
121 |
+
"""
|
122 |
+
return self.model_dump()
|
123 |
+
|
124 |
+
|
125 |
+
@track_agent(name="ChatAgent")
|
126 |
+
class ChatAgent(BaseAgent):
|
127 |
+
r"""Class for managing conversations of CAMEL Chat Agents.
|
128 |
+
|
129 |
+
Args:
|
130 |
+
system_message (Union[BaseMessage, str], optional): The system message
|
131 |
+
for the chat agent.
|
132 |
+
model (BaseModelBackend, optional): The model backend to use for
|
133 |
+
generating responses. (default: :obj:`ModelPlatformType.DEFAULT`
|
134 |
+
with `ModelType.DEFAULT`)
|
135 |
+
memory (AgentMemory, optional): The agent memory for managing chat
|
136 |
+
messages. If `None`, a :obj:`ChatHistoryMemory` will be used.
|
137 |
+
(default: :obj:`None`)
|
138 |
+
message_window_size (int, optional): The maximum number of previous
|
139 |
+
messages to include in the context window. If `None`, no windowing
|
140 |
+
is performed. (default: :obj:`None`)
|
141 |
+
token_limit (int, optional): The maximum number of tokens in a context.
|
142 |
+
The context will be automatically pruned to fulfill the limitation.
|
143 |
+
If `None`, it will be set according to the backend model.
|
144 |
+
(default: :obj:`None`)
|
145 |
+
output_language (str, optional): The language to be output by the
|
146 |
+
agent. (default: :obj:`None`)
|
147 |
+
tools (List[FunctionTool], optional): List of available
|
148 |
+
:obj:`FunctionTool`. (default: :obj:`None`)
|
149 |
+
external_tools (List[FunctionTool], optional): List of external tools
|
150 |
+
(:obj:`FunctionTool`) bind to one chat agent. When these tools
|
151 |
+
are called, the agent will directly return the request instead of
|
152 |
+
processing it. (default: :obj:`None`)
|
153 |
+
response_terminators (List[ResponseTerminator], optional): List of
|
154 |
+
:obj:`ResponseTerminator` bind to one chat agent.
|
155 |
+
(default: :obj:`None`)
|
156 |
+
scheduling_strategy (str): name of function that defines how to select
|
157 |
+
the next model in ModelManager. (default: :str:`round_robin`)
|
158 |
+
"""
|
159 |
+
|
160 |
+
def __init__(
|
161 |
+
self,
|
162 |
+
system_message: Optional[Union[BaseMessage, str]] = None,
|
163 |
+
model: Optional[
|
164 |
+
Union[BaseModelBackend, List[BaseModelBackend]]
|
165 |
+
] = None,
|
166 |
+
memory: Optional[AgentMemory] = None,
|
167 |
+
message_window_size: Optional[int] = None,
|
168 |
+
token_limit: Optional[int] = None,
|
169 |
+
output_language: Optional[str] = None,
|
170 |
+
tools: Optional[List[FunctionTool]] = None,
|
171 |
+
external_tools: Optional[List[FunctionTool]] = None,
|
172 |
+
response_terminators: Optional[List[ResponseTerminator]] = None,
|
173 |
+
scheduling_strategy: str = "round_robin",
|
174 |
+
) -> None:
|
175 |
+
from copy import deepcopy
|
176 |
+
if isinstance(system_message, str):
|
177 |
+
system_message = BaseMessage.make_assistant_message(
|
178 |
+
role_name='Assistant', content=system_message
|
179 |
+
)
|
180 |
+
|
181 |
+
self.orig_sys_message: Optional[BaseMessage] = system_message
|
182 |
+
self._system_message: Optional[BaseMessage] = system_message
|
183 |
+
self.role_name: str = (
|
184 |
+
getattr(system_message, 'role_name', None) or "assistant"
|
185 |
+
)
|
186 |
+
self.role_type: RoleType = (
|
187 |
+
getattr(system_message, 'role_type', None) or RoleType.ASSISTANT
|
188 |
+
)
|
189 |
+
self.model_backend = ModelManager(
|
190 |
+
model
|
191 |
+
if model is not None
|
192 |
+
else ModelFactory.create(
|
193 |
+
model_platform=ModelPlatformType.DEFAULT,
|
194 |
+
model_type=ModelType.DEFAULT,
|
195 |
+
),
|
196 |
+
scheduling_strategy=scheduling_strategy,
|
197 |
+
)
|
198 |
+
|
199 |
+
self.model_type = self.model_backend.model_type
|
200 |
+
|
201 |
+
# Tool registration
|
202 |
+
external_tools = external_tools or []
|
203 |
+
tools = tools or []
|
204 |
+
all_tools = tools + external_tools
|
205 |
+
self.external_tool_names = [
|
206 |
+
tool.get_function_name() for tool in external_tools
|
207 |
+
]
|
208 |
+
self.func_dict = {
|
209 |
+
tool.get_function_name(): tool.func for tool in all_tools
|
210 |
+
}
|
211 |
+
self.tool_dict = {tool.get_function_name(): tool for tool in all_tools}
|
212 |
+
self._all_tools = all_tools
|
213 |
+
|
214 |
+
# If the user set tools from `ChatAgent`, it will override the
|
215 |
+
# configured tools in `BaseModelBackend`.
|
216 |
+
if all_tools:
|
217 |
+
# logger.warning(
|
218 |
+
# "Overriding the configured tools in `BaseModelBackend` with the tools from `ChatAgent`."
|
219 |
+
# )
|
220 |
+
tool_schema_list = [
|
221 |
+
tool.get_openai_tool_schema() for tool in all_tools
|
222 |
+
]
|
223 |
+
self.model_backend.model_config_dict['tools'] = tool_schema_list
|
224 |
+
self.tool_schema_list = tool_schema_list
|
225 |
+
|
226 |
+
from copy import deepcopy
|
227 |
+
self.model_config_dict = deepcopy(self.model_backend.model_config_dict)
|
228 |
+
|
229 |
+
self.model_token_limit = token_limit or self.model_backend.token_limit
|
230 |
+
context_creator = ScoreBasedContextCreator(
|
231 |
+
self.model_backend.token_counter,
|
232 |
+
self.model_token_limit,
|
233 |
+
)
|
234 |
+
self.memory: AgentMemory = memory or ChatHistoryMemory(
|
235 |
+
context_creator, window_size=message_window_size
|
236 |
+
)
|
237 |
+
|
238 |
+
self.output_language: Optional[str] = output_language
|
239 |
+
if self.output_language is not None:
|
240 |
+
self.set_output_language(self.output_language)
|
241 |
+
|
242 |
+
self.terminated: bool = False
|
243 |
+
self.response_terminators = response_terminators or []
|
244 |
+
self.init_messages()
|
245 |
+
|
246 |
+
self.tool_prompt_added = False
|
247 |
+
|
248 |
+
# ruff: noqa: E501
|
249 |
+
def _generate_tool_prompt(self, tool_schema_list: List[Dict]) -> str:
|
250 |
+
r"""Generates a tool prompt based on the provided tool schema list.
|
251 |
+
|
252 |
+
Args:
|
253 |
+
tool_schema_list (List[Dict]): A list of dictionaries, each
|
254 |
+
containing a tool schema.
|
255 |
+
|
256 |
+
Returns:
|
257 |
+
str: A string representing the tool prompt.
|
258 |
+
"""
|
259 |
+
tool_prompts = []
|
260 |
+
|
261 |
+
for tool in tool_schema_list:
|
262 |
+
tool_info = tool['function']
|
263 |
+
tool_name = tool_info['name']
|
264 |
+
tool_description = tool_info['description']
|
265 |
+
tool_json = json.dumps(tool_info, indent=4)
|
266 |
+
|
267 |
+
prompt = f"Use the function '{tool_name}' to '{tool_description}':\n{tool_json}\n"
|
268 |
+
tool_prompts.append(prompt)
|
269 |
+
|
270 |
+
tool_prompt_str = "\n".join(tool_prompts)
|
271 |
+
|
272 |
+
final_prompt = f'''
|
273 |
+
# Tool prompt
|
274 |
+
TOOL_PROMPT = f"""
|
275 |
+
You have access to the following functions:
|
276 |
+
|
277 |
+
{tool_prompt_str}
|
278 |
+
|
279 |
+
If you choose to call a function ONLY reply in the following format with no
|
280 |
+
prefix or suffix:
|
281 |
+
|
282 |
+
<function=example_function_name>{{"example_name": "example_value"}}
|
283 |
+
</function>
|
284 |
+
|
285 |
+
Reminder:
|
286 |
+
- Function calls MUST follow the specified format, start with <function=
|
287 |
+
and end with </function>
|
288 |
+
- Required parameters MUST be specified
|
289 |
+
- Only call one function at a time
|
290 |
+
- Put the entire function call reply on one line
|
291 |
+
- If there is no function call available, answer the question like normal
|
292 |
+
with your current knowledge and do not tell the user about function calls
|
293 |
+
"""
|
294 |
+
'''
|
295 |
+
return final_prompt
|
296 |
+
|
297 |
+
def _parse_tool_response(self, response: str):
|
298 |
+
r"""Parses the tool response to extract the function name and
|
299 |
+
arguments.
|
300 |
+
|
301 |
+
Args:
|
302 |
+
response (str): The response from the model containing the
|
303 |
+
function call.
|
304 |
+
|
305 |
+
Returns:
|
306 |
+
Optional[Dict[str, Any]]: The parsed function name and arguments
|
307 |
+
if found, otherwise :obj:`None`.
|
308 |
+
"""
|
309 |
+
function_regex = r"<function=(\w+)>(.*?)</function>"
|
310 |
+
match = re.search(function_regex, response)
|
311 |
+
|
312 |
+
if match:
|
313 |
+
function_name, args_string = match.groups()
|
314 |
+
try:
|
315 |
+
args = json.loads(args_string)
|
316 |
+
return {"function": function_name, "arguments": args}
|
317 |
+
except json.JSONDecodeError as error:
|
318 |
+
print(f"Error parsing function arguments: {error}")
|
319 |
+
return None
|
320 |
+
return None
|
321 |
+
|
322 |
+
def reset(self):
|
323 |
+
r"""Resets the :obj:`ChatAgent` to its initial state."""
|
324 |
+
self.terminated = False
|
325 |
+
self.init_messages()
|
326 |
+
for terminator in self.response_terminators:
|
327 |
+
terminator.reset()
|
328 |
+
|
329 |
+
@property
|
330 |
+
def system_message(self) -> Optional[BaseMessage]:
|
331 |
+
r"""The getter method for the property :obj:`system_message`.
|
332 |
+
|
333 |
+
Returns:
|
334 |
+
Optional[BaseMessage]: The system message of this agent if set,
|
335 |
+
else :obj:`None`.
|
336 |
+
"""
|
337 |
+
return self._system_message
|
338 |
+
|
339 |
+
@system_message.setter
|
340 |
+
def system_message(self, message: BaseMessage) -> None:
|
341 |
+
r"""The setter method for the property :obj:`system_message`.
|
342 |
+
|
343 |
+
Args:
|
344 |
+
message (BaseMessage): The message to be set as the
|
345 |
+
new system message of this agent.
|
346 |
+
"""
|
347 |
+
self._system_message = message
|
348 |
+
|
349 |
+
def is_tools_added(self) -> bool:
|
350 |
+
r"""Whether OpenAI function calling is enabled for this agent.
|
351 |
+
|
352 |
+
Returns:
|
353 |
+
bool: Whether OpenAI function calling is enabled for this
|
354 |
+
agent, determined by whether the dictionary of tools
|
355 |
+
is empty.
|
356 |
+
"""
|
357 |
+
return len(self.func_dict) > 0
|
358 |
+
|
359 |
+
def update_memory(
|
360 |
+
self, message: BaseMessage, role: OpenAIBackendRole
|
361 |
+
) -> None:
|
362 |
+
r"""Updates the agent memory with a new message.
|
363 |
+
|
364 |
+
Args:
|
365 |
+
message (BaseMessage): The new message to add to the stored
|
366 |
+
messages.
|
367 |
+
role (OpenAIBackendRole): The backend role type.
|
368 |
+
"""
|
369 |
+
self.memory.write_record(
|
370 |
+
MemoryRecord(message=message, role_at_backend=role)
|
371 |
+
)
|
372 |
+
|
373 |
+
def set_output_language(self, output_language: str) -> BaseMessage:
|
374 |
+
r"""Sets the output language for the system message. This method
|
375 |
+
updates the output language for the system message. The output
|
376 |
+
language determines the language in which the output text should be
|
377 |
+
generated.
|
378 |
+
|
379 |
+
Args:
|
380 |
+
output_language (str): The desired output language.
|
381 |
+
|
382 |
+
Returns:
|
383 |
+
BaseMessage: The updated system message object.
|
384 |
+
"""
|
385 |
+
self.output_language = output_language
|
386 |
+
language_prompt = (
|
387 |
+
"\nRegardless of the input language, "
|
388 |
+
f"you must output text in {output_language}."
|
389 |
+
)
|
390 |
+
if self.orig_sys_message is not None:
|
391 |
+
content = self.orig_sys_message.content + language_prompt
|
392 |
+
self._system_message = self.orig_sys_message.create_new_instance(
|
393 |
+
content
|
394 |
+
)
|
395 |
+
else:
|
396 |
+
self._system_message = BaseMessage.make_assistant_message(
|
397 |
+
role_name="Assistant",
|
398 |
+
content=language_prompt,
|
399 |
+
)
|
400 |
+
|
401 |
+
system_record = MemoryRecord(
|
402 |
+
message=self._system_message,
|
403 |
+
role_at_backend=OpenAIBackendRole.SYSTEM,
|
404 |
+
)
|
405 |
+
self.memory.clear()
|
406 |
+
self.memory.write_record(system_record)
|
407 |
+
return self._system_message
|
408 |
+
|
409 |
+
def get_info(
|
410 |
+
self,
|
411 |
+
session_id: Optional[str],
|
412 |
+
usage: Optional[Dict[str, int]],
|
413 |
+
termination_reasons: List[str],
|
414 |
+
num_tokens: int,
|
415 |
+
tool_calls: List[FunctionCallingRecord],
|
416 |
+
external_tool_request: Optional[ChatCompletionMessageToolCall] = None,
|
417 |
+
) -> Dict[str, Any]:
|
418 |
+
r"""Returns a dictionary containing information about the chat session.
|
419 |
+
|
420 |
+
Args:
|
421 |
+
session_id (str, optional): The ID of the chat session.
|
422 |
+
usage (Dict[str, int], optional): Information about the usage of
|
423 |
+
the LLM model.
|
424 |
+
termination_reasons (List[str]): The reasons for the termination
|
425 |
+
of the chat session.
|
426 |
+
num_tokens (int): The number of tokens used in the chat session.
|
427 |
+
tool_calls (List[FunctionCallingRecord]): The list of function
|
428 |
+
calling records, containing the information of called tools.
|
429 |
+
external_tool_request
|
430 |
+
(Optional[ChatCompletionMessageToolCall], optional):
|
431 |
+
The tool calling request of external tools from the model.
|
432 |
+
These requests are directly returned to the user instead of
|
433 |
+
being processed by the agent automatically.
|
434 |
+
(default: :obj:`None`)
|
435 |
+
|
436 |
+
Returns:
|
437 |
+
Dict[str, Any]: The chat session information.
|
438 |
+
"""
|
439 |
+
return {
|
440 |
+
"id": session_id,
|
441 |
+
"usage": usage,
|
442 |
+
"termination_reasons": termination_reasons,
|
443 |
+
"num_tokens": num_tokens,
|
444 |
+
"tool_calls": tool_calls,
|
445 |
+
"external_tool_request": external_tool_request,
|
446 |
+
}
|
447 |
+
|
448 |
+
def init_messages(self) -> None:
|
449 |
+
r"""Initializes the stored messages list with the current system
|
450 |
+
message.
|
451 |
+
"""
|
452 |
+
if self._system_message is not None:
|
453 |
+
system_record = MemoryRecord(
|
454 |
+
message=self._system_message,
|
455 |
+
role_at_backend=OpenAIBackendRole.SYSTEM,
|
456 |
+
)
|
457 |
+
self.memory.clear()
|
458 |
+
self.memory.write_record(system_record)
|
459 |
+
else:
|
460 |
+
self.memory.clear()
|
461 |
+
|
462 |
+
def _transform_function_calling_format(self, openai_messages: List[dict]):
|
463 |
+
r"""Used in deepseek-chat backend. It can modify function calling records' format to match the deepseek-chat backend's format."""
|
464 |
+
from copy import deepcopy
|
465 |
+
_messages = deepcopy(openai_messages)
|
466 |
+
modified_messages = []
|
467 |
+
for message in _messages:
|
468 |
+
if message['role'] == 'function':
|
469 |
+
new_message = {
|
470 |
+
'role': 'tool',
|
471 |
+
'tool_call_id': message['name'],
|
472 |
+
'content': message['content']
|
473 |
+
}
|
474 |
+
modified_messages.append(new_message)
|
475 |
+
else:
|
476 |
+
modified_messages.append(message)
|
477 |
+
|
478 |
+
return modified_messages
|
479 |
+
|
480 |
+
|
481 |
+
def record_message(self, message: BaseMessage) -> None:
|
482 |
+
r"""Records the externally provided message into the agent memory as if
|
483 |
+
it were an answer of the :obj:`ChatAgent` from the backend. Currently,
|
484 |
+
the choice of the critic is submitted with this method.
|
485 |
+
|
486 |
+
Args:
|
487 |
+
message (BaseMessage): An external message to be recorded in the
|
488 |
+
memory.
|
489 |
+
"""
|
490 |
+
self.update_memory(message, OpenAIBackendRole.ASSISTANT)
|
491 |
+
|
492 |
+
def step(
|
493 |
+
self,
|
494 |
+
input_message: Union[BaseMessage, str],
|
495 |
+
response_format: Optional[Type[BaseModel]] = None,
|
496 |
+
) -> ChatAgentResponse:
|
497 |
+
r"""Performs a single step in the chat session by generating a response
|
498 |
+
to the input message.
|
499 |
+
|
500 |
+
Args:
|
501 |
+
input_message (Union[BaseMessage, str]): The input message to the
|
502 |
+
agent. For BaseMessage input, its `role` field that specifies
|
503 |
+
the role at backend may be either `user` or `assistant` but it
|
504 |
+
will be set to `user` anyway since for the self agent any
|
505 |
+
incoming message is external. For str input, the `role_name` would be `User`.
|
506 |
+
response_format (Optional[Type[BaseModel]], optional): A pydantic
|
507 |
+
model class that includes value types and field descriptions
|
508 |
+
used to generate a structured response by LLM. This schema
|
509 |
+
helps in defining the expected output format. (default:
|
510 |
+
:obj:`None`)
|
511 |
+
|
512 |
+
Returns:
|
513 |
+
ChatAgentResponse: A struct containing the output messages,
|
514 |
+
a boolean indicating whether the chat session has terminated,
|
515 |
+
and information about the chat session.
|
516 |
+
"""
|
517 |
+
from copy import deepcopy
|
518 |
+
self.model_backend.model_config_dict = deepcopy(self.model_config_dict)
|
519 |
+
self.tool_dict = {tool.get_function_name(): tool for tool in self._all_tools}
|
520 |
+
if (
|
521 |
+
self.model_backend.model_config_dict.get("response_format")
|
522 |
+
and response_format
|
523 |
+
):
|
524 |
+
raise ValueError(
|
525 |
+
"The `response_format` parameter cannot be set both in "
|
526 |
+
"the model configuration and in the ChatAgent step."
|
527 |
+
)
|
528 |
+
|
529 |
+
if isinstance(input_message, str):
|
530 |
+
input_message = BaseMessage.make_user_message(
|
531 |
+
role_name='User', content=input_message
|
532 |
+
)
|
533 |
+
|
534 |
+
if "llama" in self.model_type.lower():
|
535 |
+
if (
|
536 |
+
self.model_backend.model_config_dict.get("tools", None)
|
537 |
+
and not self.tool_prompt_added
|
538 |
+
):
|
539 |
+
tool_prompt = self._generate_tool_prompt(self.tool_schema_list)
|
540 |
+
|
541 |
+
tool_sys_msg = BaseMessage.make_assistant_message(
|
542 |
+
role_name="Assistant",
|
543 |
+
content=tool_prompt,
|
544 |
+
)
|
545 |
+
|
546 |
+
self.update_memory(tool_sys_msg, OpenAIBackendRole.SYSTEM)
|
547 |
+
self.tool_prompt_added = True
|
548 |
+
|
549 |
+
self.update_memory(input_message, OpenAIBackendRole.USER)
|
550 |
+
|
551 |
+
tool_call_records: List[FunctionCallingRecord] = []
|
552 |
+
while True:
|
553 |
+
# Check if token has exceeded
|
554 |
+
try:
|
555 |
+
openai_messages, num_tokens = self.memory.get_context()
|
556 |
+
except RuntimeError as e:
|
557 |
+
return self._step_token_exceed(
|
558 |
+
e.args[1], tool_call_records, "max_tokens_exceeded"
|
559 |
+
)
|
560 |
+
(
|
561 |
+
response,
|
562 |
+
output_messages,
|
563 |
+
finish_reasons,
|
564 |
+
usage_dict,
|
565 |
+
response_id,
|
566 |
+
) = self._step_model_response(openai_messages, num_tokens)
|
567 |
+
# If the model response is not a function call, meaning the
|
568 |
+
# model has generated a message response, break the loop
|
569 |
+
if (
|
570 |
+
not self.is_tools_added()
|
571 |
+
or not isinstance(response, ChatCompletion)
|
572 |
+
or "</function>" not in response.choices[0].message.content # type: ignore[operator]
|
573 |
+
):
|
574 |
+
break
|
575 |
+
|
576 |
+
parsed_content = self._parse_tool_response(
|
577 |
+
response.choices[0].message.content # type: ignore[arg-type]
|
578 |
+
)
|
579 |
+
|
580 |
+
response.choices[0].message.tool_calls = [
|
581 |
+
ChatCompletionMessageToolCall(
|
582 |
+
id=str(uuid.uuid4()),
|
583 |
+
function=Function(
|
584 |
+
arguments=str(parsed_content["arguments"]).replace(
|
585 |
+
"'", '"'
|
586 |
+
),
|
587 |
+
name=str(parsed_content["function"]),
|
588 |
+
),
|
589 |
+
type="function",
|
590 |
+
)
|
591 |
+
]
|
592 |
+
|
593 |
+
# Check for external tool call
|
594 |
+
tool_call_request = response.choices[0].message.tool_calls[0]
|
595 |
+
if tool_call_request.function.name in self.external_tool_names:
|
596 |
+
# if model calls an external tool, directly return the
|
597 |
+
# request
|
598 |
+
info = self._step_get_info(
|
599 |
+
output_messages,
|
600 |
+
finish_reasons,
|
601 |
+
usage_dict,
|
602 |
+
response_id,
|
603 |
+
tool_call_records,
|
604 |
+
num_tokens,
|
605 |
+
tool_call_request,
|
606 |
+
)
|
607 |
+
return ChatAgentResponse(
|
608 |
+
msgs=output_messages,
|
609 |
+
terminated=self.terminated,
|
610 |
+
info=info,
|
611 |
+
)
|
612 |
+
|
613 |
+
# Normal function calling
|
614 |
+
tool_call_records.append(
|
615 |
+
self._step_tool_call_and_update(response)
|
616 |
+
)
|
617 |
+
|
618 |
+
if response_format is not None:
|
619 |
+
(
|
620 |
+
output_messages,
|
621 |
+
finish_reasons,
|
622 |
+
usage_dict,
|
623 |
+
response_id,
|
624 |
+
tool_call,
|
625 |
+
num_tokens,
|
626 |
+
) = self._structure_output_with_function(response_format)
|
627 |
+
tool_call_records.append(tool_call)
|
628 |
+
|
629 |
+
info = self._step_get_info(
|
630 |
+
output_messages,
|
631 |
+
finish_reasons,
|
632 |
+
usage_dict,
|
633 |
+
response_id,
|
634 |
+
tool_call_records,
|
635 |
+
num_tokens,
|
636 |
+
)
|
637 |
+
|
638 |
+
if len(output_messages) == 1:
|
639 |
+
# Auto record if the output result is a single message
|
640 |
+
self.record_message(output_messages[0])
|
641 |
+
else:
|
642 |
+
logger.warning(
|
643 |
+
"Multiple messages returned in `step()`, message won't be "
|
644 |
+
"recorded automatically. Please call `record_message()` "
|
645 |
+
"to record the selected message manually."
|
646 |
+
)
|
647 |
+
|
648 |
+
return ChatAgentResponse(
|
649 |
+
msgs=output_messages, terminated=self.terminated, info=info
|
650 |
+
)
|
651 |
+
|
652 |
+
else:
|
653 |
+
self.update_memory(input_message, OpenAIBackendRole.USER)
|
654 |
+
# try:
|
655 |
+
|
656 |
+
tool_call_records: List[FunctionCallingRecord] = [] # type: ignore[no-redef]
|
657 |
+
while True:
|
658 |
+
# Check if token has exceeded
|
659 |
+
try:
|
660 |
+
openai_messages, num_tokens = self.memory.get_context()
|
661 |
+
except RuntimeError as e:
|
662 |
+
return self._step_token_exceed(
|
663 |
+
e.args[1], tool_call_records, "max_tokens_exceeded"
|
664 |
+
)
|
665 |
+
|
666 |
+
(
|
667 |
+
response,
|
668 |
+
output_messages,
|
669 |
+
finish_reasons,
|
670 |
+
usage_dict,
|
671 |
+
response_id,
|
672 |
+
) = self._step_model_response(openai_messages, num_tokens)
|
673 |
+
# If the model response is not a function call, meaning the
|
674 |
+
# model has generated a message response, break the loop
|
675 |
+
if (
|
676 |
+
not self.is_tools_added()
|
677 |
+
or not isinstance(response, ChatCompletion)
|
678 |
+
or not response.choices[0].message.tool_calls
|
679 |
+
):
|
680 |
+
break
|
681 |
+
|
682 |
+
# Check for external tool call
|
683 |
+
tool_call_request = response.choices[0].message.tool_calls[0]
|
684 |
+
|
685 |
+
if tool_call_request.function.name in self.external_tool_names:
|
686 |
+
# if model calls an external tool, directly return the
|
687 |
+
# request
|
688 |
+
info = self._step_get_info(
|
689 |
+
output_messages,
|
690 |
+
finish_reasons,
|
691 |
+
usage_dict,
|
692 |
+
response_id,
|
693 |
+
tool_call_records,
|
694 |
+
num_tokens,
|
695 |
+
tool_call_request,
|
696 |
+
)
|
697 |
+
return ChatAgentResponse(
|
698 |
+
msgs=output_messages,
|
699 |
+
terminated=self.terminated,
|
700 |
+
info=info,
|
701 |
+
)
|
702 |
+
|
703 |
+
# Normal function calling
|
704 |
+
tool_call_records.append(
|
705 |
+
self._step_tool_call_and_update(response)
|
706 |
+
)
|
707 |
+
|
708 |
+
if (
|
709 |
+
response_format is not None
|
710 |
+
and self.model_type.support_native_tool_calling
|
711 |
+
):
|
712 |
+
(
|
713 |
+
output_messages,
|
714 |
+
finish_reasons,
|
715 |
+
usage_dict,
|
716 |
+
response_id,
|
717 |
+
tool_call,
|
718 |
+
num_tokens,
|
719 |
+
) = self._structure_output_with_function(response_format)
|
720 |
+
tool_call_records.append(tool_call)
|
721 |
+
|
722 |
+
info = self._step_get_info(
|
723 |
+
output_messages,
|
724 |
+
finish_reasons,
|
725 |
+
usage_dict,
|
726 |
+
response_id,
|
727 |
+
tool_call_records,
|
728 |
+
num_tokens,
|
729 |
+
)
|
730 |
+
|
731 |
+
if len(output_messages) == 1:
|
732 |
+
# Auto record if the output result is a single message
|
733 |
+
self.record_message(output_messages[0])
|
734 |
+
else:
|
735 |
+
logger.warning(
|
736 |
+
"Multiple messages returned in `step()`, message won't be "
|
737 |
+
"recorded automatically. Please call `record_message()` "
|
738 |
+
"to record the selected message manually."
|
739 |
+
)
|
740 |
+
|
741 |
+
return ChatAgentResponse(
|
742 |
+
msgs=output_messages, terminated=self.terminated, info=info
|
743 |
+
)
|
744 |
+
|
745 |
+
# except Exception as e:
|
746 |
+
# logger.error(e)
|
747 |
+
# breakpoint()
|
748 |
+
# raise e
|
749 |
+
|
750 |
+
async def step_async(
|
751 |
+
self,
|
752 |
+
input_message: Union[BaseMessage, str],
|
753 |
+
response_format: Optional[Type[BaseModel]] = None,
|
754 |
+
) -> ChatAgentResponse:
|
755 |
+
r"""Performs a single step in the chat session by generating a response
|
756 |
+
to the input message. This agent step can call async function calls.
|
757 |
+
|
758 |
+
Args:
|
759 |
+
input_message (Union[BaseMessage, str]): The input message to the
|
760 |
+
agent. For BaseMessage input, its `role` field that specifies
|
761 |
+
the role at backend may be either `user` or `assistant` but it
|
762 |
+
will be set to `user` anyway since for the self agent any
|
763 |
+
incoming message is external. For str input, the `role_name` would be `User`.
|
764 |
+
response_format (Optional[Type[BaseModel]], optional): A pydantic
|
765 |
+
model class that includes value types and field descriptions
|
766 |
+
used to generate a structured response by LLM. This schema
|
767 |
+
helps in defining the expected output format. (default:
|
768 |
+
:obj:`None`)
|
769 |
+
|
770 |
+
Returns:
|
771 |
+
ChatAgentResponse: A struct containing the output messages,
|
772 |
+
a boolean indicating whether the chat session has terminated,
|
773 |
+
and information about the chat session.
|
774 |
+
"""
|
775 |
+
if isinstance(input_message, str):
|
776 |
+
input_message = BaseMessage.make_user_message(
|
777 |
+
role_name='User', content=input_message
|
778 |
+
)
|
779 |
+
|
780 |
+
self.update_memory(input_message, OpenAIBackendRole.USER)
|
781 |
+
|
782 |
+
tool_call_records: List[FunctionCallingRecord] = []
|
783 |
+
while True:
|
784 |
+
try:
|
785 |
+
openai_messages, num_tokens = self.memory.get_context()
|
786 |
+
except RuntimeError as e:
|
787 |
+
return self._step_token_exceed(
|
788 |
+
e.args[1], tool_call_records, "max_tokens_exceeded"
|
789 |
+
)
|
790 |
+
|
791 |
+
(
|
792 |
+
response,
|
793 |
+
output_messages,
|
794 |
+
finish_reasons,
|
795 |
+
usage_dict,
|
796 |
+
response_id,
|
797 |
+
) = self._step_model_response(openai_messages, num_tokens)
|
798 |
+
|
799 |
+
if (
|
800 |
+
not self.is_tools_added()
|
801 |
+
or not isinstance(response, ChatCompletion)
|
802 |
+
or response.choices[0].message.tool_calls is None
|
803 |
+
):
|
804 |
+
break
|
805 |
+
|
806 |
+
# Check for external tool call
|
807 |
+
tool_call_request = response.choices[0].message.tool_calls[0]
|
808 |
+
if tool_call_request.function.name in self.external_tool_names:
|
809 |
+
# if model calls an external tool, directly return the request
|
810 |
+
info = self._step_get_info(
|
811 |
+
output_messages,
|
812 |
+
finish_reasons,
|
813 |
+
usage_dict,
|
814 |
+
response_id,
|
815 |
+
tool_call_records,
|
816 |
+
num_tokens,
|
817 |
+
tool_call_request,
|
818 |
+
)
|
819 |
+
return ChatAgentResponse(
|
820 |
+
msgs=output_messages, terminated=self.terminated, info=info
|
821 |
+
)
|
822 |
+
|
823 |
+
# Normal function calling
|
824 |
+
tool_call_records.append(
|
825 |
+
await self._step_tool_call_and_update_async(response)
|
826 |
+
)
|
827 |
+
|
828 |
+
if (
|
829 |
+
response_format is not None
|
830 |
+
and self.model_type.support_native_tool_calling
|
831 |
+
):
|
832 |
+
(
|
833 |
+
output_messages,
|
834 |
+
finish_reasons,
|
835 |
+
usage_dict,
|
836 |
+
response_id,
|
837 |
+
tool_call_record,
|
838 |
+
num_tokens,
|
839 |
+
) = self._structure_output_with_function(response_format)
|
840 |
+
tool_call_records.append(tool_call_record)
|
841 |
+
|
842 |
+
info = self._step_get_info(
|
843 |
+
output_messages,
|
844 |
+
finish_reasons,
|
845 |
+
usage_dict,
|
846 |
+
response_id,
|
847 |
+
tool_call_records,
|
848 |
+
num_tokens,
|
849 |
+
)
|
850 |
+
|
851 |
+
if len(output_messages) == 1:
|
852 |
+
# Auto record if the output result is a single message
|
853 |
+
self.record_message(output_messages[0])
|
854 |
+
else:
|
855 |
+
logger.warning(
|
856 |
+
"Multiple messages returned in `step()`, message won't be "
|
857 |
+
"recorded automatically. Please call `record_message()` to "
|
858 |
+
"record the selected message manually."
|
859 |
+
)
|
860 |
+
|
861 |
+
return ChatAgentResponse(
|
862 |
+
msgs=output_messages, terminated=self.terminated, info=info
|
863 |
+
)
|
864 |
+
|
865 |
+
def _step_tool_call_and_update(
|
866 |
+
self, response: ChatCompletion
|
867 |
+
) -> FunctionCallingRecord:
|
868 |
+
r"""Processes a function call within the chat completion response,
|
869 |
+
records the function call in the provided list of tool calls and
|
870 |
+
updates the memory of the current agent.
|
871 |
+
|
872 |
+
Args:
|
873 |
+
response (ChatCompletion): The response object from the chat
|
874 |
+
completion.
|
875 |
+
|
876 |
+
Returns:
|
877 |
+
FunctionCallingRecord: The record of calling the function.
|
878 |
+
"""
|
879 |
+
|
880 |
+
# Perform function calling
|
881 |
+
func_assistant_msg, func_result_msg, tool_call_record = (
|
882 |
+
self.step_tool_call(response)
|
883 |
+
)
|
884 |
+
|
885 |
+
# Update the messages
|
886 |
+
self.update_memory(func_assistant_msg, OpenAIBackendRole.ASSISTANT)
|
887 |
+
self.update_memory(func_result_msg, OpenAIBackendRole.FUNCTION)
|
888 |
+
|
889 |
+
return tool_call_record
|
890 |
+
|
891 |
+
async def _step_tool_call_and_update_async(
|
892 |
+
self, response: ChatCompletion
|
893 |
+
) -> FunctionCallingRecord:
|
894 |
+
(
|
895 |
+
func_assistant_msg,
|
896 |
+
func_result_msg,
|
897 |
+
func_record,
|
898 |
+
) = await self.step_tool_call_async(response)
|
899 |
+
|
900 |
+
self.update_memory(func_assistant_msg, OpenAIBackendRole.ASSISTANT)
|
901 |
+
self.update_memory(func_result_msg, OpenAIBackendRole.FUNCTION)
|
902 |
+
|
903 |
+
return func_record
|
904 |
+
|
905 |
+
def _structure_output_with_function(
|
906 |
+
self, response_format: Type[BaseModel]
|
907 |
+
) -> Tuple[
|
908 |
+
List[BaseMessage],
|
909 |
+
List[str],
|
910 |
+
Dict[str, int],
|
911 |
+
str,
|
912 |
+
FunctionCallingRecord,
|
913 |
+
int,
|
914 |
+
]:
|
915 |
+
r"""Internal function of structuring the output of the agent based on
|
916 |
+
the given output schema.
|
917 |
+
|
918 |
+
Args:
|
919 |
+
response_format (Type[BaseModel]): The output schema to use for
|
920 |
+
structuring the output.
|
921 |
+
|
922 |
+
Returns:
|
923 |
+
Tuple[List[BaseMessage], List[str], Dict[str, int], str,
|
924 |
+
FunctionCallingRecord, int]:
|
925 |
+
A tuple containing the output messages, finish reasons, usage
|
926 |
+
dictionary, response ID, function calling record, and number of
|
927 |
+
tokens.
|
928 |
+
"""
|
929 |
+
from camel.toolkits import FunctionTool
|
930 |
+
|
931 |
+
schema_json = get_pydantic_object_schema(response_format)
|
932 |
+
func_str = json_to_function_code(schema_json)
|
933 |
+
func_callable = func_string_to_callable(func_str)
|
934 |
+
func = FunctionTool(func_callable)
|
935 |
+
|
936 |
+
original_func_dict = self.func_dict
|
937 |
+
original_model_dict = self.model_backend.model_config_dict
|
938 |
+
|
939 |
+
# Replace the original tools with the structuring function
|
940 |
+
self.func_dict = {func.get_function_name(): func.func}
|
941 |
+
self.tool_dict = {func.get_function_name(): func}
|
942 |
+
self.model_backend.model_config_dict = original_model_dict.copy()
|
943 |
+
self.model_backend.model_config_dict["tools"] = [
|
944 |
+
func.get_openai_tool_schema()
|
945 |
+
]
|
946 |
+
self.model_backend.model_config_dict["tool_choice"] = "required"
|
947 |
+
|
948 |
+
openai_messages, num_tokens = self.memory.get_context()
|
949 |
+
(
|
950 |
+
response,
|
951 |
+
output_messages,
|
952 |
+
finish_reasons,
|
953 |
+
usage_dict,
|
954 |
+
response_id,
|
955 |
+
) = self._step_model_response(openai_messages, num_tokens)
|
956 |
+
|
957 |
+
if isinstance(response, ChatCompletion):
|
958 |
+
tool_call_record = self._step_tool_call_and_update(response)
|
959 |
+
else:
|
960 |
+
raise ValueError(
|
961 |
+
"Structured output is not supported for stream responses."
|
962 |
+
)
|
963 |
+
|
964 |
+
for base_message_item in output_messages:
|
965 |
+
base_message_item.content = str(tool_call_record.result)
|
966 |
+
|
967 |
+
# Recover the original tools
|
968 |
+
self.func_dict = original_func_dict
|
969 |
+
self.model_backend.model_config_dict = original_model_dict
|
970 |
+
|
971 |
+
return (
|
972 |
+
output_messages,
|
973 |
+
finish_reasons,
|
974 |
+
usage_dict,
|
975 |
+
response_id,
|
976 |
+
tool_call_record,
|
977 |
+
num_tokens,
|
978 |
+
)
|
979 |
+
|
980 |
+
def _step_model_response(
|
981 |
+
self,
|
982 |
+
openai_messages: List[OpenAIMessage],
|
983 |
+
num_tokens: int,
|
984 |
+
) -> tuple[
|
985 |
+
Union[ChatCompletion, Stream],
|
986 |
+
List[BaseMessage],
|
987 |
+
List[str],
|
988 |
+
Dict[str, int],
|
989 |
+
str,
|
990 |
+
]:
|
991 |
+
r"""Internal function for agent step model response."""
|
992 |
+
|
993 |
+
response = None
|
994 |
+
# Obtain the model's response
|
995 |
+
for _ in range(len(self.model_backend.models)):
|
996 |
+
try:
|
997 |
+
response = self.model_backend.run(openai_messages)
|
998 |
+
break
|
999 |
+
except Exception as exc:
|
1000 |
+
logger.error(
|
1001 |
+
f"An error occurred while running model "
|
1002 |
+
f"{self.model_backend.model_type}, "
|
1003 |
+
f"index: {self.model_backend.current_model_index}",
|
1004 |
+
exc_info=exc,
|
1005 |
+
)
|
1006 |
+
continue
|
1007 |
+
if not response:
|
1008 |
+
raise ModelProcessingError(
|
1009 |
+
"Unable to process messages: none of the provided models "
|
1010 |
+
"run succesfully."
|
1011 |
+
)
|
1012 |
+
|
1013 |
+
# logger.debug(
|
1014 |
+
# f"Model {self.model_backend.model_type}, "
|
1015 |
+
# f"index {self.model_backend.current_model_index}, "
|
1016 |
+
# f"processed these messages: {openai_messages}"
|
1017 |
+
# )
|
1018 |
+
|
1019 |
+
if isinstance(response, ChatCompletion):
|
1020 |
+
output_messages, finish_reasons, usage_dict, response_id = (
|
1021 |
+
self.handle_batch_response(response)
|
1022 |
+
)
|
1023 |
+
else:
|
1024 |
+
output_messages, finish_reasons, usage_dict, response_id = (
|
1025 |
+
self.handle_stream_response(response, num_tokens)
|
1026 |
+
)
|
1027 |
+
return (
|
1028 |
+
response,
|
1029 |
+
output_messages,
|
1030 |
+
finish_reasons,
|
1031 |
+
usage_dict,
|
1032 |
+
response_id,
|
1033 |
+
)
|
1034 |
+
|
1035 |
+
def _step_get_info(
|
1036 |
+
self,
|
1037 |
+
output_messages: List[BaseMessage],
|
1038 |
+
finish_reasons: List[str],
|
1039 |
+
usage_dict: Dict[str, int],
|
1040 |
+
response_id: str,
|
1041 |
+
tool_calls: List[FunctionCallingRecord],
|
1042 |
+
num_tokens: int,
|
1043 |
+
external_tool_request: Optional[ChatCompletionMessageToolCall] = None,
|
1044 |
+
) -> Dict[str, Any]:
|
1045 |
+
r"""Process the output of a chat step and gather information about the
|
1046 |
+
step.
|
1047 |
+
|
1048 |
+
This method checks for termination conditions, updates the agent's
|
1049 |
+
state, and collects information about the chat step, including tool
|
1050 |
+
calls and termination reasons.
|
1051 |
+
|
1052 |
+
Args:
|
1053 |
+
output_messages (List[BaseMessage]): The messages generated in
|
1054 |
+
this step.
|
1055 |
+
finish_reasons (List[str]): The reasons for finishing the
|
1056 |
+
generation for each message.
|
1057 |
+
usage_dict (Dict[str, int]): Dictionary containing token usage
|
1058 |
+
information.
|
1059 |
+
response_id (str): The ID of the response from the model.
|
1060 |
+
tool_calls (List[FunctionCallingRecord]): Records of function calls
|
1061 |
+
made during this step.
|
1062 |
+
num_tokens (int): The number of tokens used in this step.
|
1063 |
+
external_tool_request (Optional[ChatCompletionMessageToolCall]):
|
1064 |
+
Any external tool request made during this step.
|
1065 |
+
(default::obj:`None`)
|
1066 |
+
|
1067 |
+
Returns:
|
1068 |
+
Dict[str, Any]: A dictionary containing information about the chat
|
1069 |
+
step, including termination status, reasons, and tool call
|
1070 |
+
information.
|
1071 |
+
|
1072 |
+
Note:
|
1073 |
+
This method iterates over all response terminators and checks if
|
1074 |
+
any of them signal termination. If a terminator signals
|
1075 |
+
termination, the agent's state is updated accordingly, and the
|
1076 |
+
termination reason is recorded.
|
1077 |
+
"""
|
1078 |
+
termination = [
|
1079 |
+
terminator.is_terminated(output_messages)
|
1080 |
+
for terminator in self.response_terminators
|
1081 |
+
]
|
1082 |
+
# Terminate the agent if any of the terminator terminates
|
1083 |
+
self.terminated, termination_reason = next(
|
1084 |
+
(
|
1085 |
+
(terminated, termination_reason)
|
1086 |
+
for terminated, termination_reason in termination
|
1087 |
+
if terminated
|
1088 |
+
),
|
1089 |
+
(False, None),
|
1090 |
+
)
|
1091 |
+
# For now only retain the first termination reason
|
1092 |
+
if self.terminated and termination_reason is not None:
|
1093 |
+
finish_reasons = [termination_reason] * len(finish_reasons)
|
1094 |
+
|
1095 |
+
info = self.get_info(
|
1096 |
+
response_id,
|
1097 |
+
usage_dict,
|
1098 |
+
finish_reasons,
|
1099 |
+
num_tokens,
|
1100 |
+
tool_calls,
|
1101 |
+
external_tool_request,
|
1102 |
+
)
|
1103 |
+
return info
|
1104 |
+
|
1105 |
+
def handle_batch_response(
|
1106 |
+
self, response: ChatCompletion
|
1107 |
+
) -> Tuple[List[BaseMessage], List[str], Dict[str, int], str]:
|
1108 |
+
r"""Process a batch response from the model and extract the necessary
|
1109 |
+
information.
|
1110 |
+
|
1111 |
+
Args:
|
1112 |
+
response (dict): Model response.
|
1113 |
+
|
1114 |
+
Returns:
|
1115 |
+
tuple: A tuple of list of output `ChatMessage`, list of
|
1116 |
+
finish reasons, usage dictionary, and response id.
|
1117 |
+
"""
|
1118 |
+
output_messages: List[BaseMessage] = []
|
1119 |
+
for choice in response.choices:
|
1120 |
+
chat_message = BaseMessage(
|
1121 |
+
role_name=self.role_name,
|
1122 |
+
role_type=self.role_type,
|
1123 |
+
meta_dict=dict(),
|
1124 |
+
content=choice.message.content or "",
|
1125 |
+
parsed=getattr(choice.message, 'parsed', None),
|
1126 |
+
)
|
1127 |
+
# Process log probabilities and append to the message meta information
|
1128 |
+
if choice.logprobs is not None:
|
1129 |
+
tokens_logprobs = choice.logprobs.content
|
1130 |
+
|
1131 |
+
if tokens_logprobs is not None:
|
1132 |
+
# Extract and structure logprob information
|
1133 |
+
logprobs_info = [
|
1134 |
+
{
|
1135 |
+
"token": token_logprob.token,
|
1136 |
+
"logprob": token_logprob.logprob,
|
1137 |
+
"top_logprobs": [
|
1138 |
+
(top_logprob.token, top_logprob.logprob)
|
1139 |
+
for top_logprob in token_logprob.top_logprobs
|
1140 |
+
],
|
1141 |
+
}
|
1142 |
+
for token_logprob in tokens_logprobs
|
1143 |
+
]
|
1144 |
+
# Ensure meta_dict exists before adding logprobs info
|
1145 |
+
if chat_message.meta_dict is None:
|
1146 |
+
chat_message.meta_dict = {}
|
1147 |
+
chat_message.meta_dict["logprobs_info"] = logprobs_info
|
1148 |
+
# Append the processed chat message to output
|
1149 |
+
output_messages.append(chat_message)
|
1150 |
+
|
1151 |
+
finish_reasons = [
|
1152 |
+
str(choice.finish_reason) for choice in response.choices
|
1153 |
+
]
|
1154 |
+
usage = (
|
1155 |
+
self._safe_model_dump(response.usage)
|
1156 |
+
if response.usage is not None
|
1157 |
+
else {}
|
1158 |
+
)
|
1159 |
+
return (
|
1160 |
+
output_messages,
|
1161 |
+
finish_reasons,
|
1162 |
+
usage,
|
1163 |
+
response.id,
|
1164 |
+
)
|
1165 |
+
|
1166 |
+
def _safe_model_dump(self, obj) -> dict:
|
1167 |
+
r"""Safely dump a Pydantic model to a dictionary.
|
1168 |
+
|
1169 |
+
This method attempts to use the `model_dump` method if available,
|
1170 |
+
otherwise it falls back to the `dict` method.
|
1171 |
+
|
1172 |
+
Args:
|
1173 |
+
obj: The Pydantic model instance to be dumped.
|
1174 |
+
|
1175 |
+
Returns:
|
1176 |
+
dict: A dictionary representation of the Pydantic model.
|
1177 |
+
"""
|
1178 |
+
# Check if the `model_dump` method exists (Pydantic v2)
|
1179 |
+
if hasattr(obj, 'model_dump'):
|
1180 |
+
return obj.model_dump()
|
1181 |
+
# Fallback to `dict()` method (Pydantic v1)
|
1182 |
+
elif hasattr(obj, 'dict'):
|
1183 |
+
return obj.dict()
|
1184 |
+
else:
|
1185 |
+
raise TypeError("The object is not a Pydantic model")
|
1186 |
+
|
1187 |
+
def handle_stream_response(
|
1188 |
+
self,
|
1189 |
+
response: Stream[ChatCompletionChunk],
|
1190 |
+
prompt_tokens: int,
|
1191 |
+
) -> Tuple[List[BaseMessage], List[str], Dict[str, int], str]:
|
1192 |
+
r"""Process a stream response from the model and extract the necessary
|
1193 |
+
information.
|
1194 |
+
|
1195 |
+
Args:
|
1196 |
+
response (dict): Model response.
|
1197 |
+
prompt_tokens (int): Number of input prompt tokens.
|
1198 |
+
|
1199 |
+
Returns:
|
1200 |
+
tuple: A tuple of list of output `ChatMessage`, list of
|
1201 |
+
finish reasons, usage dictionary, and response id.
|
1202 |
+
"""
|
1203 |
+
content_dict: defaultdict = defaultdict(lambda: "")
|
1204 |
+
finish_reasons_dict: defaultdict = defaultdict(lambda: "")
|
1205 |
+
output_messages: List[BaseMessage] = []
|
1206 |
+
response_id: str = ""
|
1207 |
+
# All choices in one response share one role
|
1208 |
+
for chunk in response:
|
1209 |
+
response_id = chunk.id
|
1210 |
+
for choice in chunk.choices:
|
1211 |
+
index = choice.index
|
1212 |
+
delta = choice.delta
|
1213 |
+
if delta.content is not None:
|
1214 |
+
# When response has not been stopped
|
1215 |
+
# Notice that only the first chunk_dict has the "role"
|
1216 |
+
content_dict[index] += delta.content
|
1217 |
+
if choice.finish_reason:
|
1218 |
+
finish_reasons_dict[index] = choice.finish_reason
|
1219 |
+
chat_message = BaseMessage(
|
1220 |
+
role_name=self.role_name,
|
1221 |
+
role_type=self.role_type,
|
1222 |
+
meta_dict=dict(),
|
1223 |
+
content=content_dict[index],
|
1224 |
+
)
|
1225 |
+
output_messages.append(chat_message)
|
1226 |
+
finish_reasons = [
|
1227 |
+
finish_reasons_dict[i] for i in range(len(finish_reasons_dict))
|
1228 |
+
]
|
1229 |
+
usage_dict = self.get_usage_dict(output_messages, prompt_tokens)
|
1230 |
+
return output_messages, finish_reasons, usage_dict, response_id
|
1231 |
+
|
1232 |
+
def _step_token_exceed(
|
1233 |
+
self,
|
1234 |
+
num_tokens: int,
|
1235 |
+
tool_calls: List[FunctionCallingRecord],
|
1236 |
+
termination_reason: str,
|
1237 |
+
) -> ChatAgentResponse:
|
1238 |
+
r"""Return trivial response containing number of tokens and information
|
1239 |
+
of called functions when the number of tokens exceeds.
|
1240 |
+
|
1241 |
+
Args:
|
1242 |
+
num_tokens (int): Number of tokens in the messages.
|
1243 |
+
tool_calls (List[FunctionCallingRecord]): List of information
|
1244 |
+
objects of functions called in the current step.
|
1245 |
+
termination_reason (str): String of termination reason.
|
1246 |
+
|
1247 |
+
Returns:
|
1248 |
+
ChatAgentResponse: The struct containing trivial outputs and
|
1249 |
+
information about token number and called functions.
|
1250 |
+
"""
|
1251 |
+
self.terminated = True
|
1252 |
+
output_messages: List[BaseMessage] = []
|
1253 |
+
|
1254 |
+
info = self.get_info(
|
1255 |
+
None,
|
1256 |
+
None,
|
1257 |
+
[termination_reason],
|
1258 |
+
num_tokens,
|
1259 |
+
tool_calls,
|
1260 |
+
)
|
1261 |
+
|
1262 |
+
return ChatAgentResponse(
|
1263 |
+
msgs=output_messages,
|
1264 |
+
terminated=self.terminated,
|
1265 |
+
info=info,
|
1266 |
+
)
|
1267 |
+
|
1268 |
+
def step_tool_call(
|
1269 |
+
self,
|
1270 |
+
response: ChatCompletion,
|
1271 |
+
) -> Tuple[
|
1272 |
+
FunctionCallingMessage, FunctionCallingMessage, FunctionCallingRecord
|
1273 |
+
]:
|
1274 |
+
r"""Execute the function with arguments following the model's response.
|
1275 |
+
|
1276 |
+
Args:
|
1277 |
+
response (Dict[str, Any]): The response obtained by calling the
|
1278 |
+
model.
|
1279 |
+
|
1280 |
+
Returns:
|
1281 |
+
tuple: A tuple consisting of two obj:`FunctionCallingMessage`,
|
1282 |
+
one about the arguments and the other about the execution
|
1283 |
+
result, and a struct for logging information about this
|
1284 |
+
function call.
|
1285 |
+
"""
|
1286 |
+
choice = response.choices[0]
|
1287 |
+
if choice.message.tool_calls is None:
|
1288 |
+
raise RuntimeError("Tool call is None")
|
1289 |
+
func_name = choice.message.tool_calls[0].function.name
|
1290 |
+
|
1291 |
+
args = json.loads(choice.message.tool_calls[0].function.arguments)
|
1292 |
+
tool = self.tool_dict[func_name]
|
1293 |
+
|
1294 |
+
result = tool(**args)
|
1295 |
+
|
1296 |
+
assist_msg = FunctionCallingMessage(
|
1297 |
+
role_name=self.role_name,
|
1298 |
+
role_type=self.role_type,
|
1299 |
+
meta_dict=None,
|
1300 |
+
content="",
|
1301 |
+
func_name=func_name,
|
1302 |
+
args=args,
|
1303 |
+
)
|
1304 |
+
func_msg = FunctionCallingMessage(
|
1305 |
+
role_name=self.role_name,
|
1306 |
+
role_type=self.role_type,
|
1307 |
+
meta_dict=None,
|
1308 |
+
content="",
|
1309 |
+
func_name=func_name,
|
1310 |
+
result=result,
|
1311 |
+
)
|
1312 |
+
|
1313 |
+
# Record information about this function call
|
1314 |
+
func_record = FunctionCallingRecord(
|
1315 |
+
func_name=func_name, args=args, result=result
|
1316 |
+
)
|
1317 |
+
return assist_msg, func_msg, func_record
|
1318 |
+
|
1319 |
+
async def step_tool_call_async(
|
1320 |
+
self,
|
1321 |
+
response: ChatCompletion,
|
1322 |
+
) -> Tuple[
|
1323 |
+
FunctionCallingMessage, FunctionCallingMessage, FunctionCallingRecord
|
1324 |
+
]:
|
1325 |
+
r"""Execute the async function with arguments following the model's
|
1326 |
+
response.
|
1327 |
+
|
1328 |
+
Args:
|
1329 |
+
response (Dict[str, Any]): The response obtained by calling the
|
1330 |
+
model.
|
1331 |
+
|
1332 |
+
Returns:
|
1333 |
+
tuple: A tuple consisting of two obj:`FunctionCallingMessage`,
|
1334 |
+
one about the arguments and the other about the execution
|
1335 |
+
result, and a struct for logging information about this
|
1336 |
+
function call.
|
1337 |
+
"""
|
1338 |
+
# Note that when function calling is enabled, `n` is set to 1.
|
1339 |
+
choice = response.choices[0]
|
1340 |
+
if choice.message.tool_calls is None:
|
1341 |
+
raise RuntimeError("Tool call is None")
|
1342 |
+
func_name = choice.message.tool_calls[0].function.name
|
1343 |
+
|
1344 |
+
args = json.loads(choice.message.tool_calls[0].function.arguments)
|
1345 |
+
tool = self.tool_dict[func_name]
|
1346 |
+
result = await tool(**args)
|
1347 |
+
|
1348 |
+
assist_msg = FunctionCallingMessage(
|
1349 |
+
role_name=self.role_name,
|
1350 |
+
role_type=self.role_type,
|
1351 |
+
meta_dict=None,
|
1352 |
+
content="",
|
1353 |
+
func_name=func_name,
|
1354 |
+
args=args,
|
1355 |
+
)
|
1356 |
+
func_msg = FunctionCallingMessage(
|
1357 |
+
role_name=self.role_name,
|
1358 |
+
role_type=self.role_type,
|
1359 |
+
meta_dict=None,
|
1360 |
+
content="",
|
1361 |
+
func_name=func_name,
|
1362 |
+
result=result,
|
1363 |
+
)
|
1364 |
+
|
1365 |
+
# Record information about this function call
|
1366 |
+
func_record = FunctionCallingRecord(
|
1367 |
+
func_name=func_name, args=args, result=result
|
1368 |
+
)
|
1369 |
+
return assist_msg, func_msg, func_record
|
1370 |
+
|
1371 |
+
def get_usage_dict(
|
1372 |
+
self, output_messages: List[BaseMessage], prompt_tokens: int
|
1373 |
+
) -> Dict[str, int]:
|
1374 |
+
r"""Get usage dictionary when using the stream mode.
|
1375 |
+
|
1376 |
+
Args:
|
1377 |
+
output_messages (list): List of output messages.
|
1378 |
+
prompt_tokens (int): Number of input prompt tokens.
|
1379 |
+
|
1380 |
+
Returns:
|
1381 |
+
dict: Usage dictionary.
|
1382 |
+
"""
|
1383 |
+
encoding = get_model_encoding(self.model_type.value_for_tiktoken)
|
1384 |
+
completion_tokens = 0
|
1385 |
+
for message in output_messages:
|
1386 |
+
completion_tokens += len(encoding.encode(message.content))
|
1387 |
+
usage_dict = dict(
|
1388 |
+
completion_tokens=completion_tokens,
|
1389 |
+
prompt_tokens=prompt_tokens,
|
1390 |
+
total_tokens=completion_tokens + prompt_tokens,
|
1391 |
+
)
|
1392 |
+
return usage_dict
|
1393 |
+
|
1394 |
+
def add_model_scheduling_strategy(self, name: str, strategy_fn: Callable):
|
1395 |
+
r"""Add a scheduling strategy method provided by user to ModelManger.
|
1396 |
+
|
1397 |
+
Args:
|
1398 |
+
name (str): The name of the strategy.
|
1399 |
+
strategy_fn (Callable): The scheduling strategy function.
|
1400 |
+
"""
|
1401 |
+
self.model_backend.add_strategy(name, strategy_fn)
|
1402 |
+
|
1403 |
+
def __repr__(self) -> str:
|
1404 |
+
r"""Returns a string representation of the :obj:`ChatAgent`.
|
1405 |
+
|
1406 |
+
Returns:
|
1407 |
+
str: The string representation of the :obj:`ChatAgent`.
|
1408 |
+
"""
|
1409 |
+
return (
|
1410 |
+
f"ChatAgent({self.role_name}, {self.role_type}, {self.model_type})"
|
1411 |
+
)
|
owl/camel/agents/critic_agent.py
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 random
|
15 |
+
import warnings
|
16 |
+
from typing import Any, Dict, Optional, Sequence
|
17 |
+
|
18 |
+
from colorama import Fore
|
19 |
+
|
20 |
+
from camel.agents.chat_agent import ChatAgent
|
21 |
+
from camel.memories import AgentMemory
|
22 |
+
from camel.messages import BaseMessage
|
23 |
+
from camel.models import BaseModelBackend
|
24 |
+
from camel.responses import ChatAgentResponse
|
25 |
+
from camel.utils import get_first_int, print_text_animated
|
26 |
+
|
27 |
+
# AgentOps decorator setting
|
28 |
+
try:
|
29 |
+
import os
|
30 |
+
|
31 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
32 |
+
from agentops import track_agent
|
33 |
+
else:
|
34 |
+
raise ImportError
|
35 |
+
except (ImportError, AttributeError):
|
36 |
+
from camel.utils import track_agent
|
37 |
+
|
38 |
+
|
39 |
+
@track_agent(name="CriticAgent")
|
40 |
+
class CriticAgent(ChatAgent):
|
41 |
+
r"""A class for the critic agent that assists in selecting an option.
|
42 |
+
|
43 |
+
Args:
|
44 |
+
system_message (BaseMessage): The system message for the critic
|
45 |
+
agent.
|
46 |
+
model (BaseModelBackend, optional): The model backend to use for
|
47 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
48 |
+
`GPT_4O_MINI`)
|
49 |
+
message_window_size (int, optional): The maximum number of previous
|
50 |
+
messages to include in the context window. If `None`, no windowing
|
51 |
+
is performed. (default: :obj:`6`)
|
52 |
+
retry_attempts (int, optional): The number of retry attempts if the
|
53 |
+
critic fails to return a valid option. (default: :obj:`2`)
|
54 |
+
verbose (bool, optional): Whether to print the critic's messages.
|
55 |
+
logger_color (Any): The color of the menu options displayed to the
|
56 |
+
user. (default: :obj:`Fore.MAGENTA`)
|
57 |
+
"""
|
58 |
+
|
59 |
+
def __init__(
|
60 |
+
self,
|
61 |
+
system_message: BaseMessage,
|
62 |
+
model: Optional[BaseModelBackend] = None,
|
63 |
+
memory: Optional[AgentMemory] = None,
|
64 |
+
message_window_size: int = 6,
|
65 |
+
retry_attempts: int = 2,
|
66 |
+
verbose: bool = False,
|
67 |
+
logger_color: Any = Fore.MAGENTA,
|
68 |
+
) -> None:
|
69 |
+
super().__init__(
|
70 |
+
system_message,
|
71 |
+
model=model,
|
72 |
+
memory=memory,
|
73 |
+
message_window_size=message_window_size,
|
74 |
+
)
|
75 |
+
self.options_dict: Dict[str, str] = dict()
|
76 |
+
self.retry_attempts = retry_attempts
|
77 |
+
self.verbose = verbose
|
78 |
+
self.logger_color = logger_color
|
79 |
+
|
80 |
+
def flatten_options(self, messages: Sequence[BaseMessage]) -> str:
|
81 |
+
r"""Flattens the options to the critic.
|
82 |
+
|
83 |
+
Args:
|
84 |
+
messages (Sequence[BaseMessage]): A list of `BaseMessage` objects.
|
85 |
+
|
86 |
+
Returns:
|
87 |
+
str: A string containing the flattened options to the critic.
|
88 |
+
"""
|
89 |
+
options = [message.content for message in messages]
|
90 |
+
flatten_options = (
|
91 |
+
f"> Proposals from "
|
92 |
+
f"{messages[0].role_name} ({messages[0].role_type}). "
|
93 |
+
"Please choose an option:\n"
|
94 |
+
)
|
95 |
+
for index, option in enumerate(options):
|
96 |
+
flatten_options += f"Option {index + 1}:\n{option}\n\n"
|
97 |
+
self.options_dict[str(index + 1)] = option
|
98 |
+
format = (
|
99 |
+
f"Please first enter your choice ([1-{len(self.options_dict)}]) "
|
100 |
+
"and then your explanation and comparison: "
|
101 |
+
)
|
102 |
+
return flatten_options + format
|
103 |
+
|
104 |
+
def get_option(self, input_message: BaseMessage) -> str:
|
105 |
+
r"""Gets the option selected by the critic.
|
106 |
+
|
107 |
+
Args:
|
108 |
+
input_message (BaseMessage): A `BaseMessage` object representing
|
109 |
+
the input message.
|
110 |
+
|
111 |
+
Returns:
|
112 |
+
str: The option selected by the critic.
|
113 |
+
"""
|
114 |
+
# TODO: Add support for editing options by the critic.
|
115 |
+
msg_content = input_message.content
|
116 |
+
i = 0
|
117 |
+
while i < self.retry_attempts:
|
118 |
+
critic_response = self.step(input_message)
|
119 |
+
|
120 |
+
if critic_response.msgs is None or len(critic_response.msgs) == 0:
|
121 |
+
raise RuntimeError("Got None critic messages.")
|
122 |
+
if critic_response.terminated:
|
123 |
+
raise RuntimeError("Critic step failed.")
|
124 |
+
|
125 |
+
critic_msg = critic_response.msg
|
126 |
+
if self.verbose:
|
127 |
+
print_text_animated(
|
128 |
+
self.logger_color + "\n> Critic response: "
|
129 |
+
f"\x1b[3m{critic_msg.content}\x1b[0m\n"
|
130 |
+
)
|
131 |
+
choice = self.parse_critic(critic_msg)
|
132 |
+
|
133 |
+
if choice in self.options_dict:
|
134 |
+
return self.options_dict[choice]
|
135 |
+
else:
|
136 |
+
input_message = BaseMessage(
|
137 |
+
role_name=input_message.role_name,
|
138 |
+
role_type=input_message.role_type,
|
139 |
+
meta_dict=input_message.meta_dict,
|
140 |
+
content="> Invalid choice. Please choose again.\n"
|
141 |
+
+ msg_content,
|
142 |
+
)
|
143 |
+
i += 1
|
144 |
+
warnings.warn(
|
145 |
+
"Critic failed to get a valid option. "
|
146 |
+
f"After {self.retry_attempts} attempts. "
|
147 |
+
"Returning a random option."
|
148 |
+
)
|
149 |
+
return random.choice(list(self.options_dict.values()))
|
150 |
+
|
151 |
+
def parse_critic(self, critic_msg: BaseMessage) -> Optional[str]:
|
152 |
+
r"""Parses the critic's message and extracts the choice.
|
153 |
+
|
154 |
+
Args:
|
155 |
+
critic_msg (BaseMessage): A `BaseMessage` object representing the
|
156 |
+
critic's response.
|
157 |
+
|
158 |
+
Returns:
|
159 |
+
Optional[str]: The critic's choice as a string, or None if the
|
160 |
+
message could not be parsed.
|
161 |
+
"""
|
162 |
+
choice = str(get_first_int(critic_msg.content))
|
163 |
+
return choice
|
164 |
+
|
165 |
+
def reduce_step(
|
166 |
+
self,
|
167 |
+
input_messages: Sequence[BaseMessage],
|
168 |
+
) -> ChatAgentResponse:
|
169 |
+
r"""Performs one step of the conversation by flattening options to the
|
170 |
+
critic, getting the option, and parsing the choice.
|
171 |
+
|
172 |
+
Args:
|
173 |
+
input_messages (Sequence[BaseMessage]): A list of BaseMessage
|
174 |
+
objects.
|
175 |
+
|
176 |
+
Returns:
|
177 |
+
ChatAgentResponse: A `ChatAgentResponse` object includes the
|
178 |
+
critic's choice.
|
179 |
+
"""
|
180 |
+
meta_chat_message = BaseMessage(
|
181 |
+
role_name=input_messages[0].role_name,
|
182 |
+
role_type=input_messages[0].role_type,
|
183 |
+
meta_dict=input_messages[0].meta_dict,
|
184 |
+
content="",
|
185 |
+
)
|
186 |
+
|
187 |
+
flatten_options = self.flatten_options(input_messages)
|
188 |
+
if self.verbose:
|
189 |
+
print_text_animated(
|
190 |
+
self.logger_color + f"\x1b[3m{flatten_options}\x1b[0m\n"
|
191 |
+
)
|
192 |
+
input_msg = meta_chat_message.create_new_instance(flatten_options)
|
193 |
+
|
194 |
+
option = self.get_option(input_msg)
|
195 |
+
output_msg = meta_chat_message.create_new_instance(option)
|
196 |
+
|
197 |
+
# TODO: The return `info` can be improved.
|
198 |
+
return ChatAgentResponse(
|
199 |
+
msgs=[output_msg],
|
200 |
+
terminated=False,
|
201 |
+
info={},
|
202 |
+
)
|
owl/camel/agents/deductive_reasoner_agent.py
ADDED
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 re
|
15 |
+
from typing import Dict, List, Optional, Union
|
16 |
+
|
17 |
+
from camel.agents.chat_agent import ChatAgent
|
18 |
+
from camel.logger import get_logger
|
19 |
+
from camel.messages import BaseMessage
|
20 |
+
from camel.models import BaseModelBackend
|
21 |
+
from camel.prompts import TextPrompt
|
22 |
+
from camel.types import RoleType
|
23 |
+
|
24 |
+
logger = get_logger(__name__)
|
25 |
+
|
26 |
+
# AgentOps decorator setting
|
27 |
+
try:
|
28 |
+
import os
|
29 |
+
|
30 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
31 |
+
from agentops import track_agent
|
32 |
+
else:
|
33 |
+
raise ImportError
|
34 |
+
except (ImportError, AttributeError):
|
35 |
+
from camel.utils import track_agent
|
36 |
+
|
37 |
+
|
38 |
+
@track_agent(name="DeductiveReasonerAgent")
|
39 |
+
class DeductiveReasonerAgent(ChatAgent):
|
40 |
+
r"""An agent responsible for deductive reasoning. Model of deductive
|
41 |
+
reasoning:
|
42 |
+
- L: A ⊕ C -> q * B
|
43 |
+
- A represents the known starting state.
|
44 |
+
- B represents the known target state.
|
45 |
+
- C represents the conditions required to transition from A to B.
|
46 |
+
- Q represents the quality or effectiveness of the transition from
|
47 |
+
A to B.
|
48 |
+
- L represents the path or process from A to B.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
model (BaseModelBackend, optional): The model backend to use for
|
52 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
53 |
+
`GPT_4O_MINI`)
|
54 |
+
"""
|
55 |
+
|
56 |
+
def __init__(
|
57 |
+
self,
|
58 |
+
model: Optional[BaseModelBackend] = None,
|
59 |
+
) -> None:
|
60 |
+
system_message = BaseMessage(
|
61 |
+
role_name="Insight Agent",
|
62 |
+
role_type=RoleType.ASSISTANT,
|
63 |
+
meta_dict=None,
|
64 |
+
content="You assign roles based on tasks.",
|
65 |
+
)
|
66 |
+
super().__init__(system_message, model=model)
|
67 |
+
|
68 |
+
def deduce_conditions_and_quality(
|
69 |
+
self,
|
70 |
+
starting_state: str,
|
71 |
+
target_state: str,
|
72 |
+
role_descriptions_dict: Optional[Dict[str, str]] = None,
|
73 |
+
) -> Dict[str, Union[List[str], Dict[str, str]]]:
|
74 |
+
r"""Derives the conditions and quality from the starting state and the
|
75 |
+
target state based on the model of the deductive reasoning and the
|
76 |
+
knowledge base. It can optionally consider the roles involved in the
|
77 |
+
scenario, which allows tailoring the output more closely to the AI
|
78 |
+
agent's environment.
|
79 |
+
|
80 |
+
Args:
|
81 |
+
starting_state (str): The initial or starting state from which
|
82 |
+
conditions are deduced.
|
83 |
+
target_state (str): The target state of the task.
|
84 |
+
role_descriptions_dict (Optional[Dict[str, str]], optional): The
|
85 |
+
descriptions of the roles. (default: :obj:`None`)
|
86 |
+
role_descriptions_dict (Optional[Dict[str, str]], optional): A
|
87 |
+
dictionary describing the roles involved in the scenario. This
|
88 |
+
is optional and can be used to provide a context for the
|
89 |
+
CAMEL's role-playing, enabling the generation of more relevant
|
90 |
+
and tailored conditions and quality assessments. This could be
|
91 |
+
generated using a `RoleAssignmentAgent()` or defined manually
|
92 |
+
by the user.
|
93 |
+
|
94 |
+
Returns:
|
95 |
+
Dict[str, Union[List[str], Dict[str, str]]]: A dictionary with the
|
96 |
+
extracted data from the message. The dictionary contains three
|
97 |
+
keys:
|
98 |
+
- 'conditions': A list where each key is a condition ID and
|
99 |
+
each value is the corresponding condition text.
|
100 |
+
- 'labels': A list of label strings extracted from the message.
|
101 |
+
- 'quality': A string of quality assessment strings extracted
|
102 |
+
from the message.
|
103 |
+
"""
|
104 |
+
self.reset()
|
105 |
+
|
106 |
+
deduce_prompt = """You are a deductive reasoner. You are tasked to
|
107 |
+
complete the TASK based on the THOUGHT OF DEDUCTIVE REASONING, the
|
108 |
+
STARTING STATE A and the TARGET STATE B. You are given the CONTEXT
|
109 |
+
CONTENT to help you complete the TASK.
|
110 |
+
Your answer MUST strictly adhere to the structure of ANSWER TEMPLATE, ONLY
|
111 |
+
fill in the BLANKs, and DO NOT alter or modify any other part of the template
|
112 |
+
|
113 |
+
===== MODELING OF DEDUCTIVE REASONING =====
|
114 |
+
You are tasked with understanding a mathematical model based on the components
|
115 |
+
${A, B, C, Q, L}$. In this model: ``L: A ⊕ C -> q * B``.
|
116 |
+
- $A$ represents the known starting state.
|
117 |
+
- $B$ represents the known target state.
|
118 |
+
- $C$ represents the conditions required to transition from $A$ to $B$.
|
119 |
+
- $Q$ represents the quality or effectiveness of the transition from $A$ to
|
120 |
+
$B$.
|
121 |
+
- $L$ represents the path or process from $A$ to $B$.
|
122 |
+
|
123 |
+
===== THOUGHT OF DEDUCTIVE REASONING =====
|
124 |
+
1. Define the Parameters of A and B:
|
125 |
+
- Characterization: Before delving into transitions, thoroughly understand
|
126 |
+
the nature and boundaries of both $A$ and $B$. This includes the type,
|
127 |
+
properties, constraints, and possible interactions between the two.
|
128 |
+
- Contrast and Compare: Highlight the similarities and differences between
|
129 |
+
$A$ and $B$. This comparative analysis will give an insight into what
|
130 |
+
needs changing and what remains constant.
|
131 |
+
2. Historical & Empirical Analysis:
|
132 |
+
- Previous Transitions according to the Knowledge Base of GPT: (if
|
133 |
+
applicable) Extract conditions and patterns from the historical instances
|
134 |
+
where a similar transition from a state comparable to $A$ moved towards
|
135 |
+
$B$.
|
136 |
+
- Scientific Principles: (if applicable) Consider the underlying
|
137 |
+
scientific principles governing or related to the states and their
|
138 |
+
transition. For example, if $A$ and $B$ are physical states, laws of
|
139 |
+
physics might apply.
|
140 |
+
3. Logical Deduction of Conditions ($C$):
|
141 |
+
- Direct Path Analysis: What are the immediate and direct conditions
|
142 |
+
required to move from $A$ to $B$?
|
143 |
+
- Intermediate States: Are there states between $A$ and $B$ that must be
|
144 |
+
traversed or can be used to make the transition smoother or more
|
145 |
+
efficient? If yes, what is the content?
|
146 |
+
- Constraints & Limitations: Identify potential barriers or restrictions
|
147 |
+
in moving from $A$ to $B$. These can be external (e.g., environmental
|
148 |
+
factors) or internal (properties of $A$ or $B$).
|
149 |
+
- Resource and Information Analysis: What resources and information are
|
150 |
+
required for the transition? This could be time, entity, factor, code
|
151 |
+
language, software platform, unknowns, etc.
|
152 |
+
- External Influences: Consider socio-economic, political, or
|
153 |
+
environmental factors (if applicable) that could influence the transition
|
154 |
+
conditions.
|
155 |
+
- Creative/Heuristic Reasoning: Open your mind to multiple possible $C$'s,
|
156 |
+
no matter how unconventional they might seem. Utilize analogies,
|
157 |
+
metaphors, or brainstorming techniques to envision possible conditions or
|
158 |
+
paths from $A$ to $B$.
|
159 |
+
- The conditions $C$ should be multiple but in one sentence. And each
|
160 |
+
condition should be concerned with one aspect/entity.
|
161 |
+
4. Entity/Label Recognition of Conditions ($C$):
|
162 |
+
- Identify and categorize entities of Conditions ($C$) such as the names,
|
163 |
+
locations, dates, specific technical terms or contextual parameters that
|
164 |
+
might be associated with events, innovations post-2022.
|
165 |
+
- The output of the entities/labels will be used as tags or labels for
|
166 |
+
semantic similarity searches. The entities/labels may be the words, or
|
167 |
+
phrases, each of them should contain valuable, high information entropy
|
168 |
+
information, and should be independent.
|
169 |
+
- Ensure that the identified entities are formatted in a manner suitable
|
170 |
+
for database indexing and retrieval. Organize the entities into
|
171 |
+
categories, and combine the category with its instance into a continuous
|
172 |
+
phrase, without using colons or other separators.
|
173 |
+
- Format these entities for database indexing: output the category rather
|
174 |
+
than its instance/content into a continuous phrase. For example, instead
|
175 |
+
of "Jan. 02", identify it as "Event time".
|
176 |
+
5. Quality Assessment ($Q$):
|
177 |
+
- Efficiency: How efficient is the transition from $A$ to $B$, which
|
178 |
+
measures the resources used versus the desired outcome?
|
179 |
+
- Effectiveness: Did the transition achieve the desired outcome or was the
|
180 |
+
target state achieved as intended?
|
181 |
+
- Safety & Risks: Assess any risks associated with the transition and the
|
182 |
+
measures to mitigate them.
|
183 |
+
- Feedback Mechanisms: Incorporate feedback loops to continuously monitor
|
184 |
+
and adjust the quality of transition, making it more adaptive.
|
185 |
+
6. Iterative Evaluation:
|
186 |
+
- Test & Refine: Based on the initially deduced conditions and assessed
|
187 |
+
quality, iterate the process to refine and optimize the transition. This
|
188 |
+
might involve tweaking conditions, employing different paths, or changing
|
189 |
+
resources.
|
190 |
+
- Feedback Integration: Use feedback to make improvements and increase the
|
191 |
+
quality of the transition.
|
192 |
+
7. Real-world scenarios often present challenges that may not be captured by
|
193 |
+
models and frameworks. While using the model, maintain an adaptive mindset:
|
194 |
+
- Scenario Exploration: Continuously imagine various possible scenarios,
|
195 |
+
both positive and negative, to prepare for unexpected events.
|
196 |
+
- Flexibility: Be prepared to modify conditions ($C$) or alter the path/
|
197 |
+
process ($L$) if unforeseen challenges arise.
|
198 |
+
- Feedback Integration: Rapidly integrate feedback from actual
|
199 |
+
implementations to adjust the model's application, ensuring relevancy and
|
200 |
+
effectiveness.
|
201 |
+
|
202 |
+
===== TASK =====
|
203 |
+
Given the starting state $A$ and the target state $B$, assuming that a path
|
204 |
+
$L$ always exists between $A$ and $B$, how can one deduce or identify the
|
205 |
+
necessary conditions $C$ and the quality $Q$ of the transition?
|
206 |
+
|
207 |
+
===== STARTING STATE $A$ =====
|
208 |
+
{starting_state}
|
209 |
+
|
210 |
+
===== TARGET STATE $B$ =====
|
211 |
+
{target_state}
|
212 |
+
|
213 |
+
{role_with_description_prompt}
|
214 |
+
===== ANSWER TEMPLATE =====
|
215 |
+
- Characterization and comparison of $A$ and $B$:\n<BLANK>
|
216 |
+
- Historical & Empirical Analysis:\n<BLANK>/None
|
217 |
+
- Logical Deduction of Conditions ($C$) (multiple conditions can be deduced):
|
218 |
+
condition <NUM>:
|
219 |
+
<BLANK>.
|
220 |
+
- Entity/Label Recognition of Conditions:\n[<BLANK>, <BLANK>, ...] (include
|
221 |
+
square brackets)
|
222 |
+
- Quality Assessment ($Q$) (do not use symbols):
|
223 |
+
<BLANK>.
|
224 |
+
- Iterative Evaluation:\n<BLANK>/None"""
|
225 |
+
|
226 |
+
if role_descriptions_dict is not None:
|
227 |
+
role_names = role_descriptions_dict.keys()
|
228 |
+
role_with_description_prompt = (
|
229 |
+
"===== ROLES WITH DESCRIPTIONS =====\n"
|
230 |
+
+ "\n".join(
|
231 |
+
f"{role_name}:\n{role_descriptions_dict[role_name]}\n"
|
232 |
+
for role_name in role_names
|
233 |
+
)
|
234 |
+
+ "\n\n"
|
235 |
+
)
|
236 |
+
else:
|
237 |
+
role_with_description_prompt = ""
|
238 |
+
deduce_prompt = TextPrompt(deduce_prompt)
|
239 |
+
|
240 |
+
deduce = deduce_prompt.format(
|
241 |
+
starting_state=starting_state,
|
242 |
+
target_state=target_state,
|
243 |
+
role_with_description_prompt=role_with_description_prompt,
|
244 |
+
)
|
245 |
+
|
246 |
+
conditions_and_quality_generation_msg = BaseMessage.make_user_message(
|
247 |
+
role_name="Deductive Reasoner", content=deduce
|
248 |
+
)
|
249 |
+
|
250 |
+
response = self.step(
|
251 |
+
input_message=conditions_and_quality_generation_msg
|
252 |
+
)
|
253 |
+
|
254 |
+
if response.terminated:
|
255 |
+
raise RuntimeError(
|
256 |
+
"Deduction failed. Error:\n" + f"{response.info}"
|
257 |
+
)
|
258 |
+
msg: BaseMessage = response.msg
|
259 |
+
logger.info(f"Message content:\n{msg.content}")
|
260 |
+
|
261 |
+
# Extract the conditions from the message
|
262 |
+
conditions_dict = {
|
263 |
+
f"condition {i}": cdt.replace("<", "")
|
264 |
+
.replace(">", "")
|
265 |
+
.strip()
|
266 |
+
.strip('\n')
|
267 |
+
for i, cdt in re.findall(
|
268 |
+
r"condition (\d+):\s*(.+?)(?=condition \d+|- Entity)",
|
269 |
+
msg.content,
|
270 |
+
re.DOTALL,
|
271 |
+
)
|
272 |
+
}
|
273 |
+
|
274 |
+
# Extract the labels from the message
|
275 |
+
labels = [
|
276 |
+
label.strip().strip('\n').strip("\"'")
|
277 |
+
for label in re.findall(
|
278 |
+
r"Entity/Label Recognition of Conditions:\n\[(.+?)\]",
|
279 |
+
msg.content,
|
280 |
+
re.DOTALL,
|
281 |
+
)[0].split(",")
|
282 |
+
]
|
283 |
+
|
284 |
+
# Extract the quality from the message
|
285 |
+
quality = next(
|
286 |
+
q.strip().strip('\n')
|
287 |
+
for q in re.findall(
|
288 |
+
r"Quality Assessment \(\$Q\$\) \(do not use symbols\):"
|
289 |
+
r"\n(.+?)- Iterative",
|
290 |
+
msg.content,
|
291 |
+
re.DOTALL,
|
292 |
+
)
|
293 |
+
)
|
294 |
+
|
295 |
+
# Convert them into JSON format
|
296 |
+
conditions_and_quality_json: Dict[
|
297 |
+
str, Union[List[str], Dict[str, str]]
|
298 |
+
] = {}
|
299 |
+
conditions_and_quality_json["conditions"] = conditions_dict
|
300 |
+
conditions_and_quality_json["labels"] = labels
|
301 |
+
conditions_and_quality_json["evaluate_quality"] = quality
|
302 |
+
|
303 |
+
return conditions_and_quality_json
|
owl/camel/agents/embodied_agent.py
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Any, List, Optional
|
15 |
+
|
16 |
+
from colorama import Fore
|
17 |
+
|
18 |
+
from camel.agents.chat_agent import ChatAgent
|
19 |
+
from camel.agents.tool_agents.base import BaseToolAgent
|
20 |
+
from camel.interpreters import (
|
21 |
+
BaseInterpreter,
|
22 |
+
InternalPythonInterpreter,
|
23 |
+
SubprocessInterpreter,
|
24 |
+
)
|
25 |
+
from camel.messages import BaseMessage
|
26 |
+
from camel.models import BaseModelBackend
|
27 |
+
from camel.responses import ChatAgentResponse
|
28 |
+
from camel.utils import print_text_animated
|
29 |
+
|
30 |
+
# AgentOps decorator setting
|
31 |
+
try:
|
32 |
+
import os
|
33 |
+
|
34 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
35 |
+
from agentops import track_agent
|
36 |
+
else:
|
37 |
+
raise ImportError
|
38 |
+
except (ImportError, AttributeError):
|
39 |
+
from camel.utils import track_agent
|
40 |
+
|
41 |
+
|
42 |
+
@track_agent(name="EmbodiedAgent")
|
43 |
+
class EmbodiedAgent(ChatAgent):
|
44 |
+
r"""Class for managing conversations of CAMEL Embodied Agents.
|
45 |
+
|
46 |
+
Args:
|
47 |
+
system_message (BaseMessage): The system message for the chat agent.
|
48 |
+
model (BaseModelBackend, optional): The model backend to use for
|
49 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
50 |
+
`GPT_4O_MINI`)
|
51 |
+
message_window_size (int, optional): The maximum number of previous
|
52 |
+
messages to include in the context window. If `None`, no windowing
|
53 |
+
is performed. (default: :obj:`None`)
|
54 |
+
tool_agents (List[BaseToolAgent], optional): The tools agents to use in
|
55 |
+
the embodied agent. (default: :obj:`None`)
|
56 |
+
code_interpreter (BaseInterpreter, optional): The code interpreter to
|
57 |
+
execute codes. If `code_interpreter` and `tool_agent` are both
|
58 |
+
`None`, default to `SubProcessInterpreter`. If `code_interpreter`
|
59 |
+
is `None` and `tool_agents` is not `None`, default to
|
60 |
+
`InternalPythonInterpreter`. (default: :obj:`None`)
|
61 |
+
verbose (bool, optional): Whether to print the critic's messages.
|
62 |
+
logger_color (Any): The color of the logger displayed to the user.
|
63 |
+
(default: :obj:`Fore.MAGENTA`)
|
64 |
+
"""
|
65 |
+
|
66 |
+
def __init__(
|
67 |
+
self,
|
68 |
+
system_message: BaseMessage,
|
69 |
+
model: Optional[BaseModelBackend] = None,
|
70 |
+
message_window_size: Optional[int] = None,
|
71 |
+
tool_agents: Optional[List[BaseToolAgent]] = None,
|
72 |
+
code_interpreter: Optional[BaseInterpreter] = None,
|
73 |
+
verbose: bool = False,
|
74 |
+
logger_color: Any = Fore.MAGENTA,
|
75 |
+
) -> None:
|
76 |
+
self.tool_agents = tool_agents
|
77 |
+
self.code_interpreter: BaseInterpreter
|
78 |
+
if code_interpreter is not None:
|
79 |
+
self.code_interpreter = code_interpreter
|
80 |
+
elif self.tool_agents:
|
81 |
+
self.code_interpreter = InternalPythonInterpreter()
|
82 |
+
else:
|
83 |
+
self.code_interpreter = SubprocessInterpreter()
|
84 |
+
|
85 |
+
if self.tool_agents:
|
86 |
+
system_message = self._set_tool_agents(system_message)
|
87 |
+
self.verbose = verbose
|
88 |
+
self.logger_color = logger_color
|
89 |
+
super().__init__(
|
90 |
+
system_message=system_message,
|
91 |
+
model=model,
|
92 |
+
message_window_size=message_window_size,
|
93 |
+
)
|
94 |
+
|
95 |
+
def _set_tool_agents(self, system_message: BaseMessage) -> BaseMessage:
|
96 |
+
action_space_prompt = self._get_tool_agents_prompt()
|
97 |
+
result_message = system_message.create_new_instance(
|
98 |
+
content=system_message.content.format(
|
99 |
+
action_space=action_space_prompt
|
100 |
+
)
|
101 |
+
)
|
102 |
+
if self.tool_agents is not None:
|
103 |
+
self.code_interpreter.update_action_space(
|
104 |
+
{tool.name: tool for tool in self.tool_agents}
|
105 |
+
)
|
106 |
+
return result_message
|
107 |
+
|
108 |
+
def _get_tool_agents_prompt(self) -> str:
|
109 |
+
r"""Returns the action space prompt.
|
110 |
+
|
111 |
+
Returns:
|
112 |
+
str: The action space prompt.
|
113 |
+
"""
|
114 |
+
if self.tool_agents is not None:
|
115 |
+
return "\n".join(
|
116 |
+
[
|
117 |
+
f"*** {tool.name} ***:\n {tool.description}"
|
118 |
+
for tool in self.tool_agents
|
119 |
+
]
|
120 |
+
)
|
121 |
+
else:
|
122 |
+
return ""
|
123 |
+
|
124 |
+
def get_tool_agent_names(self) -> List[str]:
|
125 |
+
r"""Returns the names of tool agents.
|
126 |
+
|
127 |
+
Returns:
|
128 |
+
List[str]: The names of tool agents.
|
129 |
+
"""
|
130 |
+
if self.tool_agents is not None:
|
131 |
+
return [tool.name for tool in self.tool_agents]
|
132 |
+
else:
|
133 |
+
return []
|
134 |
+
|
135 |
+
# ruff: noqa: E501
|
136 |
+
def step(self, input_message: BaseMessage) -> ChatAgentResponse: # type: ignore[override]
|
137 |
+
r"""Performs a step in the conversation.
|
138 |
+
|
139 |
+
Args:
|
140 |
+
input_message (BaseMessage): The input message.
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
ChatAgentResponse: A struct containing the output messages,
|
144 |
+
a boolean indicating whether the chat session has terminated,
|
145 |
+
and information about the chat session.
|
146 |
+
"""
|
147 |
+
response = super().step(input_message)
|
148 |
+
|
149 |
+
if response.msgs is None or len(response.msgs) == 0:
|
150 |
+
raise RuntimeError("Got None output messages.")
|
151 |
+
if response.terminated:
|
152 |
+
raise RuntimeError(f"{self.__class__.__name__} step failed.")
|
153 |
+
|
154 |
+
# NOTE: Only single output messages are supported
|
155 |
+
explanations, codes = response.msg.extract_text_and_code_prompts()
|
156 |
+
|
157 |
+
if self.verbose:
|
158 |
+
for explanation, code in zip(explanations, codes):
|
159 |
+
print_text_animated(
|
160 |
+
self.logger_color + f"> Explanation:\n{explanation}"
|
161 |
+
)
|
162 |
+
print_text_animated(self.logger_color + f"> Code:\n{code}")
|
163 |
+
|
164 |
+
if len(explanations) > len(codes):
|
165 |
+
print_text_animated(
|
166 |
+
self.logger_color + f"> Explanation:\n{explanations[-1]}"
|
167 |
+
)
|
168 |
+
|
169 |
+
content = response.msg.content
|
170 |
+
|
171 |
+
if codes is not None:
|
172 |
+
try:
|
173 |
+
content = "\n> Executed Results:\n"
|
174 |
+
for block_idx, code in enumerate(codes):
|
175 |
+
executed_output = self.code_interpreter.run(
|
176 |
+
code, code.code_type
|
177 |
+
)
|
178 |
+
content += (
|
179 |
+
f"Executing code block {block_idx}: {{\n"
|
180 |
+
+ executed_output
|
181 |
+
+ "}\n"
|
182 |
+
)
|
183 |
+
except InterruptedError as e:
|
184 |
+
content = (
|
185 |
+
f"\n> Running code fail: {e}\n"
|
186 |
+
"Please regenerate the code."
|
187 |
+
)
|
188 |
+
|
189 |
+
# TODO: Handle errors
|
190 |
+
content = input_message.content + f"\n> Embodied Actions:\n{content}"
|
191 |
+
message = BaseMessage(
|
192 |
+
input_message.role_name,
|
193 |
+
input_message.role_type,
|
194 |
+
input_message.meta_dict,
|
195 |
+
content,
|
196 |
+
)
|
197 |
+
return ChatAgentResponse(
|
198 |
+
msgs=[message],
|
199 |
+
terminated=response.terminated,
|
200 |
+
info=response.info,
|
201 |
+
)
|
owl/camel/agents/knowledge_graph_agent.py
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import TYPE_CHECKING, Optional, Union
|
15 |
+
|
16 |
+
if TYPE_CHECKING:
|
17 |
+
from unstructured.documents.elements import Element
|
18 |
+
|
19 |
+
from camel.agents import ChatAgent
|
20 |
+
from camel.messages import BaseMessage
|
21 |
+
from camel.models import BaseModelBackend
|
22 |
+
from camel.prompts import TextPrompt
|
23 |
+
from camel.storages.graph_storages.graph_element import (
|
24 |
+
GraphElement,
|
25 |
+
Node,
|
26 |
+
Relationship,
|
27 |
+
)
|
28 |
+
from camel.types import RoleType
|
29 |
+
|
30 |
+
# AgentOps decorator setting
|
31 |
+
try:
|
32 |
+
import os
|
33 |
+
|
34 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
35 |
+
from agentops import track_agent
|
36 |
+
else:
|
37 |
+
raise ImportError
|
38 |
+
except (ImportError, AttributeError):
|
39 |
+
from camel.utils import track_agent
|
40 |
+
|
41 |
+
|
42 |
+
text_prompt = """
|
43 |
+
You are tasked with extracting nodes and relationships from given content and
|
44 |
+
structures them into Node and Relationship objects. Here's the outline of what
|
45 |
+
you needs to do:
|
46 |
+
|
47 |
+
Content Extraction:
|
48 |
+
You should be able to process input content and identify entities mentioned
|
49 |
+
within it.
|
50 |
+
Entities can be any noun phrases or concepts that represent distinct entities
|
51 |
+
in the context of the given content.
|
52 |
+
|
53 |
+
Node Extraction:
|
54 |
+
For each identified entity, you should create a Node object.
|
55 |
+
Each Node object should have a unique identifier (id) and a type (type).
|
56 |
+
Additional properties associated with the node can also be extracted and
|
57 |
+
stored.
|
58 |
+
|
59 |
+
Relationship Extraction:
|
60 |
+
You should identify relationships between entities mentioned in the content.
|
61 |
+
For each relationship, create a Relationship object.
|
62 |
+
A Relationship object should have a subject (subj) and an object (obj) which
|
63 |
+
are Node objects representing the entities involved in the relationship.
|
64 |
+
Each relationship should also have a type (type), and additional properties if
|
65 |
+
applicable.
|
66 |
+
|
67 |
+
Output Formatting:
|
68 |
+
The extracted nodes and relationships should be formatted as instances of the
|
69 |
+
provided Node and Relationship classes.
|
70 |
+
Ensure that the extracted data adheres to the structure defined by the classes.
|
71 |
+
Output the structured data in a format that can be easily validated against
|
72 |
+
the provided code.
|
73 |
+
|
74 |
+
Instructions for you:
|
75 |
+
Read the provided content thoroughly.
|
76 |
+
Identify distinct entities mentioned in the content and categorize them as
|
77 |
+
nodes.
|
78 |
+
Determine relationships between these entities and represent them as directed
|
79 |
+
relationships.
|
80 |
+
Provide the extracted nodes and relationships in the specified format below.
|
81 |
+
Example for you:
|
82 |
+
|
83 |
+
Example Content:
|
84 |
+
"John works at XYZ Corporation. He is a software engineer. The company is
|
85 |
+
located in New York City."
|
86 |
+
|
87 |
+
Expected Output:
|
88 |
+
|
89 |
+
Nodes:
|
90 |
+
|
91 |
+
Node(id='John', type='Person')
|
92 |
+
Node(id='XYZ Corporation', type='Organization')
|
93 |
+
Node(id='New York City', type='Location')
|
94 |
+
|
95 |
+
Relationships:
|
96 |
+
|
97 |
+
Relationship(subj=Node(id='John', type='Person'), obj=Node(id='XYZ
|
98 |
+
Corporation', type='Organization'), type='WorksAt')
|
99 |
+
Relationship(subj=Node(id='John', type='Person'), obj=Node(id='New York City',
|
100 |
+
type='Location'), type='ResidesIn')
|
101 |
+
|
102 |
+
===== TASK =====
|
103 |
+
Please extracts nodes and relationships from given content and structures them
|
104 |
+
into Node and Relationship objects.
|
105 |
+
|
106 |
+
{task}
|
107 |
+
"""
|
108 |
+
|
109 |
+
|
110 |
+
@track_agent(name="KnowledgeGraphAgent")
|
111 |
+
class KnowledgeGraphAgent(ChatAgent):
|
112 |
+
r"""An agent that can extract node and relationship information for
|
113 |
+
different entities from given `Element` content.
|
114 |
+
|
115 |
+
Attributes:
|
116 |
+
task_prompt (TextPrompt): A prompt for the agent to extract node and
|
117 |
+
relationship information for different entities.
|
118 |
+
"""
|
119 |
+
|
120 |
+
def __init__(
|
121 |
+
self,
|
122 |
+
model: Optional[BaseModelBackend] = None,
|
123 |
+
) -> None:
|
124 |
+
r"""Initialize the `KnowledgeGraphAgent`.
|
125 |
+
|
126 |
+
Args:
|
127 |
+
model (BaseModelBackend, optional): The model backend to use for
|
128 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
129 |
+
`GPT_4O_MINI`)
|
130 |
+
"""
|
131 |
+
system_message = BaseMessage(
|
132 |
+
role_name="Graphify",
|
133 |
+
role_type=RoleType.ASSISTANT,
|
134 |
+
meta_dict=None,
|
135 |
+
content="Your mission is to transform unstructured content "
|
136 |
+
"into structured graph data. Extract nodes and relationships with "
|
137 |
+
"precision, and let the connections unfold. Your graphs will "
|
138 |
+
"illuminate the hidden connections within the chaos of "
|
139 |
+
"information.",
|
140 |
+
)
|
141 |
+
super().__init__(system_message, model=model)
|
142 |
+
|
143 |
+
def run(
|
144 |
+
self,
|
145 |
+
element: "Element",
|
146 |
+
parse_graph_elements: bool = False,
|
147 |
+
) -> Union[str, GraphElement]:
|
148 |
+
r"""Run the agent to extract node and relationship information.
|
149 |
+
|
150 |
+
Args:
|
151 |
+
element (Element): The input element.
|
152 |
+
parse_graph_elements (bool, optional): Whether to parse into
|
153 |
+
`GraphElement`. Defaults to `False`.
|
154 |
+
|
155 |
+
Returns:
|
156 |
+
Union[str, GraphElement]: The extracted node and relationship
|
157 |
+
information. If `parse_graph_elements` is `True` then return
|
158 |
+
`GraphElement`, else return `str`.
|
159 |
+
"""
|
160 |
+
self.reset()
|
161 |
+
self.element = element
|
162 |
+
|
163 |
+
knowledge_graph_prompt = TextPrompt(text_prompt)
|
164 |
+
knowledge_graph_generation = knowledge_graph_prompt.format(
|
165 |
+
task=str(element)
|
166 |
+
)
|
167 |
+
|
168 |
+
knowledge_graph_generation_msg = BaseMessage.make_user_message(
|
169 |
+
role_name="Graphify", content=knowledge_graph_generation
|
170 |
+
)
|
171 |
+
|
172 |
+
response = self.step(input_message=knowledge_graph_generation_msg)
|
173 |
+
|
174 |
+
content = response.msg.content
|
175 |
+
|
176 |
+
if parse_graph_elements:
|
177 |
+
content = self._parse_graph_elements(content)
|
178 |
+
|
179 |
+
return content
|
180 |
+
|
181 |
+
def _validate_node(self, node: Node) -> bool:
|
182 |
+
r"""Validate if the object is a valid Node.
|
183 |
+
|
184 |
+
Args:
|
185 |
+
node (Node): Object to be validated.
|
186 |
+
|
187 |
+
Returns:
|
188 |
+
bool: True if the object is a valid Node, False otherwise.
|
189 |
+
"""
|
190 |
+
return (
|
191 |
+
isinstance(node, Node)
|
192 |
+
and isinstance(node.id, (str, int))
|
193 |
+
and isinstance(node.type, str)
|
194 |
+
)
|
195 |
+
|
196 |
+
def _validate_relationship(self, relationship: Relationship) -> bool:
|
197 |
+
r"""Validate if the object is a valid Relationship.
|
198 |
+
|
199 |
+
Args:
|
200 |
+
relationship (Relationship): Object to be validated.
|
201 |
+
|
202 |
+
Returns:
|
203 |
+
bool: True if the object is a valid Relationship, False otherwise.
|
204 |
+
"""
|
205 |
+
return (
|
206 |
+
isinstance(relationship, Relationship)
|
207 |
+
and self._validate_node(relationship.subj)
|
208 |
+
and self._validate_node(relationship.obj)
|
209 |
+
and isinstance(relationship.type, str)
|
210 |
+
)
|
211 |
+
|
212 |
+
def _parse_graph_elements(self, input_string: str) -> GraphElement:
|
213 |
+
r"""Parses graph elements from given content.
|
214 |
+
|
215 |
+
Args:
|
216 |
+
input_string (str): The input content.
|
217 |
+
|
218 |
+
Returns:
|
219 |
+
GraphElement: The parsed graph elements.
|
220 |
+
"""
|
221 |
+
import re
|
222 |
+
|
223 |
+
# Regular expressions to extract nodes and relationships
|
224 |
+
node_pattern = r"Node\(id='(.*?)', type='(.*?)'\)"
|
225 |
+
rel_pattern = (
|
226 |
+
r"Relationship\(subj=Node\(id='(.*?)', type='(.*?)'\), "
|
227 |
+
r"obj=Node\(id='(.*?)', type='(.*?)'\), type='(.*?)'\)"
|
228 |
+
)
|
229 |
+
|
230 |
+
nodes = {}
|
231 |
+
relationships = []
|
232 |
+
|
233 |
+
# Extract nodes
|
234 |
+
for match in re.finditer(node_pattern, input_string):
|
235 |
+
id, type = match.groups()
|
236 |
+
properties = {'source': 'agent_created'}
|
237 |
+
if id not in nodes:
|
238 |
+
node = Node(id=id, type=type, properties=properties)
|
239 |
+
if self._validate_node(node):
|
240 |
+
nodes[id] = node
|
241 |
+
|
242 |
+
# Extract relationships
|
243 |
+
for match in re.finditer(rel_pattern, input_string):
|
244 |
+
subj_id, subj_type, obj_id, obj_type, rel_type = match.groups()
|
245 |
+
properties = {'source': 'agent_created'}
|
246 |
+
if subj_id in nodes and obj_id in nodes:
|
247 |
+
subj = nodes[subj_id]
|
248 |
+
obj = nodes[obj_id]
|
249 |
+
relationship = Relationship(
|
250 |
+
subj=subj, obj=obj, type=rel_type, properties=properties
|
251 |
+
)
|
252 |
+
if self._validate_relationship(relationship):
|
253 |
+
relationships.append(relationship)
|
254 |
+
|
255 |
+
return GraphElement(
|
256 |
+
nodes=list(nodes.values()),
|
257 |
+
relationships=relationships,
|
258 |
+
source=self.element,
|
259 |
+
)
|
owl/camel/agents/role_assignment_agent.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 re
|
15 |
+
from typing import Dict, Optional, Union
|
16 |
+
|
17 |
+
from camel.agents.chat_agent import ChatAgent
|
18 |
+
from camel.messages import BaseMessage
|
19 |
+
from camel.models import BaseModelBackend
|
20 |
+
from camel.prompts import TextPrompt
|
21 |
+
from camel.types import RoleType
|
22 |
+
|
23 |
+
# AgentOps decorator setting
|
24 |
+
try:
|
25 |
+
import os
|
26 |
+
|
27 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
28 |
+
from agentops import track_agent
|
29 |
+
else:
|
30 |
+
raise ImportError
|
31 |
+
except (ImportError, AttributeError):
|
32 |
+
from camel.utils import track_agent
|
33 |
+
|
34 |
+
|
35 |
+
@track_agent(name="RoleAssignmentAgent")
|
36 |
+
class RoleAssignmentAgent(ChatAgent):
|
37 |
+
r"""An agent that generates role names based on the task prompt.
|
38 |
+
|
39 |
+
Args:
|
40 |
+
model (BaseModelBackend, optional): The model backend to use for
|
41 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
42 |
+
`GPT_4O_MINI`)
|
43 |
+
|
44 |
+
Attributes:
|
45 |
+
role_assignment_prompt (TextPrompt): A prompt for the agent to generate
|
46 |
+
role names.
|
47 |
+
"""
|
48 |
+
|
49 |
+
def __init__(
|
50 |
+
self,
|
51 |
+
model: Optional[BaseModelBackend] = None,
|
52 |
+
) -> None:
|
53 |
+
system_message = BaseMessage(
|
54 |
+
role_name="Role Assigner",
|
55 |
+
role_type=RoleType.ASSISTANT,
|
56 |
+
meta_dict=None,
|
57 |
+
content="You assign roles based on tasks.",
|
58 |
+
)
|
59 |
+
super().__init__(system_message, model=model)
|
60 |
+
|
61 |
+
def run(
|
62 |
+
self,
|
63 |
+
task_prompt: Union[str, TextPrompt],
|
64 |
+
num_roles: int = 2,
|
65 |
+
) -> Dict[str, str]:
|
66 |
+
r"""Generate role names based on the input task prompt.
|
67 |
+
|
68 |
+
Args:
|
69 |
+
task_prompt (Union[str, TextPrompt]): The prompt
|
70 |
+
for the task based on which the roles are to be generated.
|
71 |
+
num_roles (int, optional): The number of roles to generate.
|
72 |
+
(default: :obj:`2`)
|
73 |
+
|
74 |
+
Returns:
|
75 |
+
Dict[str, str]: A dictionary mapping role names to their
|
76 |
+
descriptions.
|
77 |
+
"""
|
78 |
+
self.reset()
|
79 |
+
|
80 |
+
expert_prompt = "===== ANSWER PROMPT =====\n" + "\n".join(
|
81 |
+
f"Domain expert {i + 1}: <BLANK>\n"
|
82 |
+
f"Associated competencies, characteristics, duties "
|
83 |
+
f"and workflows: <BLANK>. End."
|
84 |
+
for i in range(num_roles or 0)
|
85 |
+
)
|
86 |
+
role_assignment_generation_prompt = TextPrompt(
|
87 |
+
"You are a role assignment agent, and you're in charge of "
|
88 |
+
+ "recruiting {num_roles} experts for the following task."
|
89 |
+
+ "\n==== TASK =====\n {task}\n\n"
|
90 |
+
+ "Identify the domain experts you'd recruit and detail their "
|
91 |
+
+ "associated competencies, characteristics, duties and workflows "
|
92 |
+
+ "to complete the task.\n "
|
93 |
+
+ "Your answer MUST adhere to the format of ANSWER PROMPT, and "
|
94 |
+
+ "ONLY answer the BLANKs.\n"
|
95 |
+
+ expert_prompt
|
96 |
+
)
|
97 |
+
role_assignment_generation = role_assignment_generation_prompt.format(
|
98 |
+
num_roles=num_roles, task=task_prompt
|
99 |
+
)
|
100 |
+
|
101 |
+
role_assignment_generation_msg = BaseMessage.make_user_message(
|
102 |
+
role_name="Role Assigner", content=role_assignment_generation
|
103 |
+
)
|
104 |
+
|
105 |
+
response = self.step(input_message=role_assignment_generation_msg)
|
106 |
+
|
107 |
+
msg = response.msg # type: BaseMessage
|
108 |
+
terminated = response.terminated
|
109 |
+
|
110 |
+
# Distribute the output completions into role names and descriptions
|
111 |
+
role_names = [
|
112 |
+
desc.replace("<|", "").replace("|>", "")
|
113 |
+
for desc in re.findall(
|
114 |
+
r"Domain expert \d: (.+?)\nAssociated competencies,",
|
115 |
+
msg.content,
|
116 |
+
re.DOTALL,
|
117 |
+
)
|
118 |
+
]
|
119 |
+
role_descriptions = [
|
120 |
+
desc.replace("<|", "").replace("|>", "")
|
121 |
+
for desc in re.findall(
|
122 |
+
r"Associated competencies, characteristics, "
|
123 |
+
r"duties and workflows: (.+?) End.",
|
124 |
+
msg.content,
|
125 |
+
re.DOTALL,
|
126 |
+
)
|
127 |
+
]
|
128 |
+
|
129 |
+
if len(role_names) != num_roles or len(role_descriptions) != num_roles:
|
130 |
+
raise RuntimeError(
|
131 |
+
"Got None or insufficient information of roles."
|
132 |
+
)
|
133 |
+
if terminated:
|
134 |
+
raise RuntimeError("Role assignment failed.")
|
135 |
+
|
136 |
+
role_descriptions_dict = {
|
137 |
+
role_name: description
|
138 |
+
for role_name, description in zip(role_names, role_descriptions)
|
139 |
+
}
|
140 |
+
|
141 |
+
return role_descriptions_dict
|
owl/camel/agents/search_agent.py
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Optional
|
15 |
+
|
16 |
+
from camel.agents.chat_agent import ChatAgent
|
17 |
+
from camel.messages import BaseMessage
|
18 |
+
from camel.models import BaseModelBackend
|
19 |
+
from camel.prompts import TextPrompt
|
20 |
+
from camel.types import RoleType
|
21 |
+
from camel.utils import create_chunks
|
22 |
+
|
23 |
+
# AgentOps decorator setting
|
24 |
+
try:
|
25 |
+
import os
|
26 |
+
|
27 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
28 |
+
from agentops import track_agent
|
29 |
+
else:
|
30 |
+
raise ImportError
|
31 |
+
except (ImportError, AttributeError):
|
32 |
+
from camel.utils import track_agent
|
33 |
+
|
34 |
+
|
35 |
+
@track_agent(name="SearchAgent")
|
36 |
+
class SearchAgent(ChatAgent):
|
37 |
+
r"""An agent that summarizes text based on a query and evaluates the
|
38 |
+
relevance of an answer.
|
39 |
+
|
40 |
+
Args:
|
41 |
+
model (BaseModelBackend, optional): The model backend to use for
|
42 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
43 |
+
`GPT_4O_MINI`)
|
44 |
+
"""
|
45 |
+
|
46 |
+
def __init__(
|
47 |
+
self,
|
48 |
+
model: Optional[BaseModelBackend] = None,
|
49 |
+
) -> None:
|
50 |
+
system_message = BaseMessage(
|
51 |
+
role_name="Assistant",
|
52 |
+
role_type=RoleType.ASSISTANT,
|
53 |
+
meta_dict=None,
|
54 |
+
content="You are a helpful assistant.",
|
55 |
+
)
|
56 |
+
super().__init__(system_message, model=model)
|
57 |
+
|
58 |
+
def summarize_text(self, text: str, query: str) -> str:
|
59 |
+
r"""Summarize the information from the text, base on the query.
|
60 |
+
|
61 |
+
Args:
|
62 |
+
text (str): Text to summarize.
|
63 |
+
query (str): What information you want.
|
64 |
+
|
65 |
+
Returns:
|
66 |
+
str: Strings with information.
|
67 |
+
"""
|
68 |
+
self.reset()
|
69 |
+
|
70 |
+
summary_prompt = TextPrompt(
|
71 |
+
'''Gather information from this text that relative to the
|
72 |
+
question, but do not directly answer the question.\nquestion:
|
73 |
+
{query}\ntext '''
|
74 |
+
)
|
75 |
+
summary_prompt = summary_prompt.format(query=query)
|
76 |
+
# Max length of each chunk
|
77 |
+
max_len = 3000
|
78 |
+
results = ""
|
79 |
+
chunks = create_chunks(text, max_len)
|
80 |
+
# Summarize
|
81 |
+
for i, chunk in enumerate(chunks, start=1):
|
82 |
+
prompt = summary_prompt + str(i) + ": " + chunk
|
83 |
+
user_msg = BaseMessage.make_user_message(
|
84 |
+
role_name="User",
|
85 |
+
content=prompt,
|
86 |
+
)
|
87 |
+
result = self.step(user_msg).msg.content
|
88 |
+
results += result + "\n"
|
89 |
+
|
90 |
+
# Final summarization
|
91 |
+
final_prompt = TextPrompt(
|
92 |
+
'''Here are some summarized texts which split from one text. Using
|
93 |
+
the information to answer the question. If can't find the answer,
|
94 |
+
you must answer "I can not find the answer to the query" and
|
95 |
+
explain why.\n Query:\n{query}.\n\nText:\n'''
|
96 |
+
)
|
97 |
+
final_prompt = final_prompt.format(query=query)
|
98 |
+
prompt = final_prompt + results
|
99 |
+
|
100 |
+
user_msg = BaseMessage.make_user_message(
|
101 |
+
role_name="User",
|
102 |
+
content=prompt,
|
103 |
+
)
|
104 |
+
response = self.step(user_msg).msg.content
|
105 |
+
|
106 |
+
return response
|
107 |
+
|
108 |
+
def continue_search(self, query: str, answer: str) -> bool:
|
109 |
+
r"""Ask whether to continue search or not based on the provided answer.
|
110 |
+
|
111 |
+
Args:
|
112 |
+
query (str): The question.
|
113 |
+
answer (str): The answer to the question.
|
114 |
+
|
115 |
+
Returns:
|
116 |
+
bool: `True` if the user want to continue search, `False`
|
117 |
+
otherwise.
|
118 |
+
"""
|
119 |
+
prompt = TextPrompt(
|
120 |
+
"Do you think the ANSWER can answer the QUERY? "
|
121 |
+
"Use only 'yes' or 'no' to answer.\n"
|
122 |
+
"===== QUERY =====\n{query}\n\n"
|
123 |
+
"===== ANSWER =====\n{answer}"
|
124 |
+
)
|
125 |
+
prompt = prompt.format(query=query, answer=answer)
|
126 |
+
user_msg = BaseMessage.make_user_message(
|
127 |
+
role_name="User",
|
128 |
+
content=prompt,
|
129 |
+
)
|
130 |
+
response = self.step(user_msg).msg.content
|
131 |
+
if "yes" in str(response).lower():
|
132 |
+
return False
|
133 |
+
return True
|
owl/camel/agents/task_agent.py
ADDED
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Any, Dict, List, Optional, Union
|
15 |
+
|
16 |
+
from camel.agents.chat_agent import ChatAgent
|
17 |
+
from camel.messages import BaseMessage
|
18 |
+
from camel.models import BaseModelBackend
|
19 |
+
from camel.prompts import PromptTemplateGenerator, TextPrompt
|
20 |
+
from camel.types import RoleType, TaskType
|
21 |
+
from camel.utils import get_task_list
|
22 |
+
|
23 |
+
# AgentOps decorator setting
|
24 |
+
try:
|
25 |
+
import os
|
26 |
+
|
27 |
+
if os.getenv("AGENTOPS_API_KEY") is not None:
|
28 |
+
from agentops import track_agent
|
29 |
+
else:
|
30 |
+
raise ImportError
|
31 |
+
except (ImportError, AttributeError):
|
32 |
+
from camel.utils import track_agent
|
33 |
+
|
34 |
+
|
35 |
+
@track_agent(name="TaskSpecifyAgent")
|
36 |
+
class TaskSpecifyAgent(ChatAgent):
|
37 |
+
r"""An agent that specifies a given task prompt by prompting the user to
|
38 |
+
provide more details.
|
39 |
+
|
40 |
+
Attributes:
|
41 |
+
DEFAULT_WORD_LIMIT (int): The default word limit for the task prompt.
|
42 |
+
task_specify_prompt (TextPrompt): The prompt for specifying the task.
|
43 |
+
|
44 |
+
Args:
|
45 |
+
model (BaseModelBackend, optional): The model backend to use for
|
46 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
47 |
+
`GPT_4O_MINI`)
|
48 |
+
task_type (TaskType, optional): The type of task for which to generate
|
49 |
+
a prompt. (default: :obj:`TaskType.AI_SOCIETY`)
|
50 |
+
task_specify_prompt (Union[str, TextPrompt], optional): The prompt for
|
51 |
+
specifying the task. (default: :obj:`None`)
|
52 |
+
word_limit (int, optional): The word limit for the task prompt.
|
53 |
+
(default: :obj:`50`)
|
54 |
+
output_language (str, optional): The language to be output by the
|
55 |
+
agent. (default: :obj:`None`)
|
56 |
+
"""
|
57 |
+
|
58 |
+
DEFAULT_WORD_LIMIT = 50
|
59 |
+
|
60 |
+
def __init__(
|
61 |
+
self,
|
62 |
+
model: Optional[BaseModelBackend] = None,
|
63 |
+
task_type: TaskType = TaskType.AI_SOCIETY,
|
64 |
+
task_specify_prompt: Optional[Union[str, TextPrompt]] = None,
|
65 |
+
word_limit: int = DEFAULT_WORD_LIMIT,
|
66 |
+
output_language: Optional[str] = None,
|
67 |
+
) -> None:
|
68 |
+
self.task_specify_prompt: Union[str, TextPrompt]
|
69 |
+
if task_specify_prompt is None:
|
70 |
+
task_specify_prompt_template = (
|
71 |
+
PromptTemplateGenerator().get_task_specify_prompt(task_type)
|
72 |
+
)
|
73 |
+
|
74 |
+
self.task_specify_prompt = task_specify_prompt_template.format(
|
75 |
+
word_limit=word_limit
|
76 |
+
)
|
77 |
+
else:
|
78 |
+
self.task_specify_prompt = TextPrompt(task_specify_prompt)
|
79 |
+
|
80 |
+
system_message = BaseMessage(
|
81 |
+
role_name="Task Specifier",
|
82 |
+
role_type=RoleType.ASSISTANT,
|
83 |
+
meta_dict=None,
|
84 |
+
content="You can make a task more specific.",
|
85 |
+
)
|
86 |
+
|
87 |
+
super().__init__(
|
88 |
+
system_message,
|
89 |
+
model=model,
|
90 |
+
output_language=output_language,
|
91 |
+
)
|
92 |
+
|
93 |
+
def run(
|
94 |
+
self,
|
95 |
+
task_prompt: Union[str, TextPrompt],
|
96 |
+
meta_dict: Optional[Dict[str, Any]] = None,
|
97 |
+
) -> TextPrompt:
|
98 |
+
r"""Specify the given task prompt by providing more details.
|
99 |
+
|
100 |
+
Args:
|
101 |
+
task_prompt (Union[str, TextPrompt]): The original task
|
102 |
+
prompt.
|
103 |
+
meta_dict (Dict[str, Any], optional): A dictionary containing
|
104 |
+
additional information to include in the prompt.
|
105 |
+
(default: :obj:`None`)
|
106 |
+
|
107 |
+
Returns:
|
108 |
+
TextPrompt: The specified task prompt.
|
109 |
+
"""
|
110 |
+
self.reset()
|
111 |
+
task_specify_prompt = self.task_specify_prompt.format(task=task_prompt)
|
112 |
+
|
113 |
+
if meta_dict is not None:
|
114 |
+
task_specify_prompt = task_specify_prompt.format(**meta_dict)
|
115 |
+
task_msg = BaseMessage.make_user_message(
|
116 |
+
role_name="Task Specifier", content=task_specify_prompt
|
117 |
+
)
|
118 |
+
specifier_response = self.step(task_msg)
|
119 |
+
|
120 |
+
if specifier_response.terminated:
|
121 |
+
raise RuntimeError("Task specification failed.")
|
122 |
+
if len(specifier_response.msgs) == 0:
|
123 |
+
raise RuntimeError("Got no specification message.")
|
124 |
+
|
125 |
+
specified_task_msg = specifier_response.msgs[0]
|
126 |
+
|
127 |
+
return TextPrompt(specified_task_msg.content)
|
128 |
+
|
129 |
+
|
130 |
+
@track_agent(name="TaskPlannerAgent")
|
131 |
+
class TaskPlannerAgent(ChatAgent):
|
132 |
+
r"""An agent that helps divide a task into subtasks based on the input
|
133 |
+
task prompt.
|
134 |
+
|
135 |
+
Attributes:
|
136 |
+
task_planner_prompt (TextPrompt): A prompt for the agent to divide
|
137 |
+
the task into subtasks.
|
138 |
+
|
139 |
+
Args:
|
140 |
+
model (BaseModelBackend, optional): The model backend to use for
|
141 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
142 |
+
`GPT_4O_MINI`)
|
143 |
+
output_language (str, optional): The language to be output by the
|
144 |
+
agent. (default: :obj:`None`)
|
145 |
+
"""
|
146 |
+
|
147 |
+
def __init__(
|
148 |
+
self,
|
149 |
+
model: Optional[BaseModelBackend] = None,
|
150 |
+
output_language: Optional[str] = None,
|
151 |
+
) -> None:
|
152 |
+
self.task_planner_prompt = TextPrompt(
|
153 |
+
"Divide this task into subtasks: {task}. Be concise."
|
154 |
+
)
|
155 |
+
system_message = BaseMessage(
|
156 |
+
role_name="Task Planner",
|
157 |
+
role_type=RoleType.ASSISTANT,
|
158 |
+
meta_dict=None,
|
159 |
+
content="You are a helpful task planner.",
|
160 |
+
)
|
161 |
+
|
162 |
+
super().__init__(
|
163 |
+
system_message,
|
164 |
+
model=model,
|
165 |
+
output_language=output_language,
|
166 |
+
)
|
167 |
+
|
168 |
+
def run(
|
169 |
+
self,
|
170 |
+
task_prompt: Union[str, TextPrompt],
|
171 |
+
) -> TextPrompt:
|
172 |
+
r"""Generate subtasks based on the input task prompt.
|
173 |
+
|
174 |
+
Args:
|
175 |
+
task_prompt (Union[str, TextPrompt]): The prompt for the task to
|
176 |
+
be divided into subtasks.
|
177 |
+
|
178 |
+
Returns:
|
179 |
+
TextPrompt: A prompt for the subtasks generated by the agent.
|
180 |
+
"""
|
181 |
+
# TODO: Maybe include roles information.
|
182 |
+
self.reset()
|
183 |
+
task_planner_prompt = self.task_planner_prompt.format(task=task_prompt)
|
184 |
+
|
185 |
+
task_msg = BaseMessage.make_user_message(
|
186 |
+
role_name="Task Planner", content=task_planner_prompt
|
187 |
+
)
|
188 |
+
|
189 |
+
task_response = self.step(task_msg)
|
190 |
+
|
191 |
+
if task_response.terminated:
|
192 |
+
raise RuntimeError("Task planning failed.")
|
193 |
+
if len(task_response.msgs) == 0:
|
194 |
+
raise RuntimeError("Got no task planning message.")
|
195 |
+
|
196 |
+
sub_tasks_msg = task_response.msgs[0]
|
197 |
+
return TextPrompt(sub_tasks_msg.content)
|
198 |
+
|
199 |
+
|
200 |
+
@track_agent(name="TaskCreationAgent")
|
201 |
+
class TaskCreationAgent(ChatAgent):
|
202 |
+
r"""An agent that helps create new tasks based on the objective
|
203 |
+
and last completed task. Compared to :obj:`TaskPlannerAgent`,
|
204 |
+
it's still a task planner, but it has more context information
|
205 |
+
like last task and incomplete task list. Modified from
|
206 |
+
`BabyAGI <https://github.com/yoheinakajima/babyagi>`_.
|
207 |
+
|
208 |
+
Attributes:
|
209 |
+
task_creation_prompt (TextPrompt): A prompt for the agent to
|
210 |
+
create new tasks.
|
211 |
+
|
212 |
+
Args:
|
213 |
+
role_name (str): The role name of the Agent to create the task.
|
214 |
+
objective (Union[str, TextPrompt]): The objective of the Agent to
|
215 |
+
perform the task.
|
216 |
+
model (BaseModelBackend, optional): The LLM backend to use for
|
217 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
218 |
+
`GPT_4O_MINI`)
|
219 |
+
output_language (str, optional): The language to be output by the
|
220 |
+
agent. (default: :obj:`None`)
|
221 |
+
message_window_size (int, optional): The maximum number of previous
|
222 |
+
messages to include in the context window. If `None`, no windowing
|
223 |
+
is performed. (default: :obj:`None`)
|
224 |
+
max_task_num (int, optional): The maximum number of planned
|
225 |
+
tasks in one round. (default: :obj:3)
|
226 |
+
"""
|
227 |
+
|
228 |
+
def __init__(
|
229 |
+
self,
|
230 |
+
role_name: str,
|
231 |
+
objective: Union[str, TextPrompt],
|
232 |
+
model: Optional[BaseModelBackend] = None,
|
233 |
+
output_language: Optional[str] = None,
|
234 |
+
message_window_size: Optional[int] = None,
|
235 |
+
max_task_num: Optional[int] = 3,
|
236 |
+
) -> None:
|
237 |
+
task_creation_prompt = TextPrompt(
|
238 |
+
"""Create new a task with the following objective: {objective}.
|
239 |
+
Never forget you are a Task Creator of {role_name}.
|
240 |
+
You must instruct me based on my expertise and your needs to solve the task.
|
241 |
+
You should consider past solved tasks and in-progress tasks: {task_list}.
|
242 |
+
The new created tasks must not overlap with these past tasks.
|
243 |
+
The result must be a numbered list in the format:
|
244 |
+
|
245 |
+
#. First Task
|
246 |
+
#. Second Task
|
247 |
+
#. Third Task
|
248 |
+
|
249 |
+
You can only give me up to {max_task_num} tasks at a time. \
|
250 |
+
Each task should be concise, concrete and doable for a {role_name}.
|
251 |
+
You should make task plan and not ask me questions.
|
252 |
+
If you think no new tasks are needed right now, write "No tasks to add."
|
253 |
+
Now start to give me new tasks one by one. No more than three tasks.
|
254 |
+
Be concrete.
|
255 |
+
"""
|
256 |
+
)
|
257 |
+
|
258 |
+
self.task_creation_prompt = task_creation_prompt.format(
|
259 |
+
objective=objective, role_name=role_name, max_task_num=max_task_num
|
260 |
+
)
|
261 |
+
self.objective = objective
|
262 |
+
|
263 |
+
system_message = BaseMessage(
|
264 |
+
role_name="Task Creator",
|
265 |
+
role_type=RoleType.ASSISTANT,
|
266 |
+
meta_dict=None,
|
267 |
+
content="You are a helpful task creator.",
|
268 |
+
)
|
269 |
+
|
270 |
+
super().__init__(
|
271 |
+
system_message,
|
272 |
+
model=model,
|
273 |
+
output_language=output_language,
|
274 |
+
message_window_size=message_window_size,
|
275 |
+
)
|
276 |
+
|
277 |
+
def run(
|
278 |
+
self,
|
279 |
+
task_list: List[str],
|
280 |
+
) -> List[str]:
|
281 |
+
r"""Generate subtasks based on the previous task results and
|
282 |
+
incomplete task list.
|
283 |
+
|
284 |
+
Args:
|
285 |
+
task_list (List[str]): The completed or in-progress
|
286 |
+
tasks which should not overlap with new created tasks.
|
287 |
+
|
288 |
+
Returns:
|
289 |
+
List[str]: The new task list generated by the Agent.
|
290 |
+
"""
|
291 |
+
|
292 |
+
if len(task_list) > 0:
|
293 |
+
task_creation_prompt = self.task_creation_prompt.format(
|
294 |
+
task_list=task_list
|
295 |
+
)
|
296 |
+
else:
|
297 |
+
task_creation_prompt = self.task_creation_prompt.format(
|
298 |
+
task_list=""
|
299 |
+
)
|
300 |
+
|
301 |
+
task_msg = BaseMessage.make_user_message(
|
302 |
+
role_name="Task Creator", content=task_creation_prompt
|
303 |
+
)
|
304 |
+
task_response = self.step(task_msg)
|
305 |
+
|
306 |
+
if task_response.terminated:
|
307 |
+
raise RuntimeError("Task creation failed.")
|
308 |
+
if len(task_response.msgs) == 0:
|
309 |
+
raise RuntimeError("Got no task creation message.")
|
310 |
+
|
311 |
+
sub_tasks_msg = task_response.msgs[0]
|
312 |
+
return get_task_list(sub_tasks_msg.content)
|
313 |
+
|
314 |
+
|
315 |
+
@track_agent(name="TaskPrioritizationAgent")
|
316 |
+
class TaskPrioritizationAgent(ChatAgent):
|
317 |
+
r"""An agent that helps re-prioritize the task list and
|
318 |
+
returns numbered prioritized list. Modified from
|
319 |
+
`BabyAGI <https://github.com/yoheinakajima/babyagi>`_.
|
320 |
+
|
321 |
+
Attributes:
|
322 |
+
task_prioritization_prompt (TextPrompt): A prompt for the agent to
|
323 |
+
prioritize tasks.
|
324 |
+
|
325 |
+
Args:
|
326 |
+
objective (Union[str, TextPrompt]): The objective of the Agent to
|
327 |
+
perform the task.
|
328 |
+
model (BaseModelBackend, optional): The LLM backend to use for
|
329 |
+
generating responses. (default: :obj:`OpenAIModel` with
|
330 |
+
`GPT_4O_MINI`)
|
331 |
+
output_language (str, optional): The language to be output by the
|
332 |
+
agent. (default: :obj:`None`)
|
333 |
+
message_window_size (int, optional): The maximum number of previous
|
334 |
+
messages to include in the context window. If `None`, no windowing
|
335 |
+
is performed. (default: :obj:`None`)
|
336 |
+
"""
|
337 |
+
|
338 |
+
def __init__(
|
339 |
+
self,
|
340 |
+
objective: Union[str, TextPrompt],
|
341 |
+
model: Optional[BaseModelBackend] = None,
|
342 |
+
output_language: Optional[str] = None,
|
343 |
+
message_window_size: Optional[int] = None,
|
344 |
+
) -> None:
|
345 |
+
task_prioritization_prompt = TextPrompt(
|
346 |
+
"""Prioritize the following tasks : {task_list}.
|
347 |
+
Consider the ultimate objective of you: {objective}.
|
348 |
+
Tasks should be sorted from highest to lowest priority, where higher-priority \
|
349 |
+
tasks are those that act as pre-requisites or are more essential for meeting \
|
350 |
+
the objective. Return one task per line in your response.
|
351 |
+
Do not remove or modify any tasks.
|
352 |
+
The result must be a numbered list in the format:
|
353 |
+
|
354 |
+
#. First task
|
355 |
+
#. Second task
|
356 |
+
|
357 |
+
The entries must be consecutively numbered, starting with 1.
|
358 |
+
The number of each entry must be followed by a period.
|
359 |
+
Do not include any headers before your ranked list or follow your list \
|
360 |
+
with any other output."""
|
361 |
+
)
|
362 |
+
|
363 |
+
self.task_prioritization_prompt = task_prioritization_prompt.format(
|
364 |
+
objective=objective
|
365 |
+
)
|
366 |
+
self.objective = objective
|
367 |
+
|
368 |
+
system_message = BaseMessage(
|
369 |
+
role_name="Task Prioritizer",
|
370 |
+
role_type=RoleType.ASSISTANT,
|
371 |
+
meta_dict=None,
|
372 |
+
content="You are a helpful task prioritizer.",
|
373 |
+
)
|
374 |
+
|
375 |
+
super().__init__(
|
376 |
+
system_message,
|
377 |
+
model=model,
|
378 |
+
output_language=output_language,
|
379 |
+
message_window_size=message_window_size,
|
380 |
+
)
|
381 |
+
|
382 |
+
def run(
|
383 |
+
self,
|
384 |
+
task_list: List[str],
|
385 |
+
) -> List[str]:
|
386 |
+
r"""Prioritize the task list given the agent objective.
|
387 |
+
|
388 |
+
Args:
|
389 |
+
task_list (List[str]): The unprioritized tasks of agent.
|
390 |
+
|
391 |
+
Returns:
|
392 |
+
List[str]: The new prioritized task list generated by the Agent.
|
393 |
+
"""
|
394 |
+
task_prioritization_prompt = self.task_prioritization_prompt.format(
|
395 |
+
task_list=task_list
|
396 |
+
)
|
397 |
+
|
398 |
+
task_msg = BaseMessage.make_user_message(
|
399 |
+
role_name="Task Prioritizer", content=task_prioritization_prompt
|
400 |
+
)
|
401 |
+
|
402 |
+
task_response = self.step(task_msg)
|
403 |
+
|
404 |
+
if task_response.terminated:
|
405 |
+
raise RuntimeError("Task prioritization failed.")
|
406 |
+
if len(task_response.msgs) == 0:
|
407 |
+
raise RuntimeError("Got no task prioritization message.")
|
408 |
+
|
409 |
+
sub_tasks_msg = task_response.msgs[0]
|
410 |
+
return get_task_list(sub_tasks_msg.content)
|
owl/camel/agents/tool_agents/__init__.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .base import BaseToolAgent
|
15 |
+
from .hugging_face_tool_agent import HuggingFaceToolAgent
|
16 |
+
|
17 |
+
__all__ = [
|
18 |
+
'BaseToolAgent',
|
19 |
+
'HuggingFaceToolAgent',
|
20 |
+
]
|
owl/camel/agents/tool_agents/base.py
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from camel.agents import BaseAgent
|
15 |
+
|
16 |
+
|
17 |
+
class BaseToolAgent(BaseAgent):
|
18 |
+
r"""Creates a :obj:`BaseToolAgent` object with the specified name and
|
19 |
+
description.
|
20 |
+
|
21 |
+
Args:
|
22 |
+
name (str): The name of the tool agent.
|
23 |
+
description (str): The description of the tool agent.
|
24 |
+
"""
|
25 |
+
|
26 |
+
def __init__(self, name: str, description: str) -> None:
|
27 |
+
self.name = name
|
28 |
+
self.description = description
|
29 |
+
|
30 |
+
def reset(self) -> None:
|
31 |
+
r"""Resets the agent to its initial state."""
|
32 |
+
pass
|
33 |
+
|
34 |
+
def step(self) -> None:
|
35 |
+
r"""Performs a single step of the agent."""
|
36 |
+
pass
|
37 |
+
|
38 |
+
def __str__(self) -> str:
|
39 |
+
return f"{self.name}: {self.description}"
|
owl/camel/agents/tool_agents/hugging_face_tool_agent.py
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Any, Optional
|
15 |
+
|
16 |
+
from camel.agents.tool_agents.base import BaseToolAgent
|
17 |
+
|
18 |
+
|
19 |
+
# flake8: noqa :E501
|
20 |
+
class HuggingFaceToolAgent(BaseToolAgent):
|
21 |
+
r"""Tool agent for calling HuggingFace models. This agent is a wrapper
|
22 |
+
around agents from the `transformers` library. For more information
|
23 |
+
about the available models, please see the `transformers` documentation
|
24 |
+
at https://huggingface.co/docs/transformers/transformers_agents.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
name (str): The name of the agent.
|
28 |
+
*args (Any): Additional positional arguments to pass to the underlying
|
29 |
+
Agent class.
|
30 |
+
remote (bool, optional): Flag indicating whether to run the agent
|
31 |
+
remotely. (default: :obj:`True`)
|
32 |
+
**kwargs (Any): Additional keyword arguments to pass to the underlying
|
33 |
+
Agent class.
|
34 |
+
"""
|
35 |
+
|
36 |
+
def __init__(
|
37 |
+
self,
|
38 |
+
name: str,
|
39 |
+
*args: Any,
|
40 |
+
remote: bool = True,
|
41 |
+
**kwargs: Any,
|
42 |
+
) -> None:
|
43 |
+
try:
|
44 |
+
# TODO: Support other tool agents
|
45 |
+
import transformers
|
46 |
+
from packaging import version
|
47 |
+
|
48 |
+
if version.parse(transformers.__version__) < version.parse(
|
49 |
+
"4.31.0"
|
50 |
+
):
|
51 |
+
raise ValueError(
|
52 |
+
"The version of \"transformers\" package should >= 4.31.0"
|
53 |
+
)
|
54 |
+
|
55 |
+
from transformers.tools import OpenAiAgent
|
56 |
+
from transformers.tools.agent_types import AgentImage
|
57 |
+
except (ImportError, ValueError):
|
58 |
+
raise ValueError(
|
59 |
+
"Could not import transformers tool agents. "
|
60 |
+
"Please setup the environment with "
|
61 |
+
"pip install huggingface_hub==0.14.1 transformers==4.31.0 diffusers accelerate==0.20.3 datasets torch soundfile sentencepiece opencv-python"
|
62 |
+
)
|
63 |
+
self.agent_image_type = AgentImage
|
64 |
+
self.agent = OpenAiAgent(*args, **kwargs)
|
65 |
+
description = f"""The `{name}` is a tool agent that can perform a variety of tasks including:
|
66 |
+
- Document question answering: given a document (such as a PDF) in image format, answer a question on this document
|
67 |
+
- Text question answering: given a long text and a question, answer the question in the text
|
68 |
+
- Unconditional image captioning: Caption the image!
|
69 |
+
- Image question answering: given an image, answer a question on this image
|
70 |
+
- Image segmentation: given an image and a prompt, output the segmentation mask of that prompt
|
71 |
+
- Speech to text: given an audio recording of a person talking, transcribe the speech into text
|
72 |
+
- Text to speech: convert text to speech
|
73 |
+
- Zero-shot text classification: given a text and a list of labels, identify to which label the text corresponds the most
|
74 |
+
- Text summarization: summarize a long text in one or a few sentences
|
75 |
+
- Translation: translate the text into a given language
|
76 |
+
- Text downloading: to download a text from a web URL
|
77 |
+
- Text to image: generate an image according to a prompt, leveraging stable diffusion
|
78 |
+
- Image transformation: modify an image given an initial image and a prompt, leveraging instruct pix2pix stable diffusion
|
79 |
+
- Text to video: generate a small video according to a prompt
|
80 |
+
|
81 |
+
Here are some python code examples of what you can do with this agent:
|
82 |
+
|
83 |
+
Single execution (step) mode, the single execution method is when using the step() method of the agent:
|
84 |
+
```
|
85 |
+
# Text to image
|
86 |
+
rivers_and_lakes_image = {name}.step("Draw me a picture of rivers and lakes.")
|
87 |
+
rivers_and_lakes_image.save("./rivers_and_lakes_image.png")
|
88 |
+
|
89 |
+
# Text to image -> Image transformation
|
90 |
+
sea_add_island_image = {name}.step("Draw me a picture of the sea then transform the picture to add an island")
|
91 |
+
sea_add_island_image.save("./sea_add_island_image.png")
|
92 |
+
|
93 |
+
# If you'd like to keep a state across executions or to pass non-text objects to the agent,
|
94 |
+
# you can do so by specifying variables that you would like the agent to use. For example,
|
95 |
+
# you could generate the first image of rivers and lakes, and ask the model to update that picture to add an island by doing the following:
|
96 |
+
picture = {name}.step("Generate a picture of rivers and lakes.")
|
97 |
+
picture.save("./picture.png")
|
98 |
+
updated_picture = {name}.step("Transform the image in `picture` to add an island to it.", picture=picture)
|
99 |
+
updated_picture.save("./updated_picture.png")
|
100 |
+
|
101 |
+
capybara_sea_image = {name}.step("Draw me a picture of the `prompt`", prompt="a capybara swimming in the sea")
|
102 |
+
capybara_sea_image.save("./capybara_sea_image.png")
|
103 |
+
|
104 |
+
# Document question answering
|
105 |
+
answer = {name}.step(
|
106 |
+
"In the following `document`, where will the TRRF Scientific Advisory Council Meeting take place?",
|
107 |
+
document=document,
|
108 |
+
)
|
109 |
+
print(answer)
|
110 |
+
|
111 |
+
|
112 |
+
# Text to image
|
113 |
+
boat_image = {name}.step("Generate an image of a boat in the water")
|
114 |
+
boat_image.save("./boat_image.png")
|
115 |
+
|
116 |
+
# Unconditional image captioning
|
117 |
+
boat_image_caption = {name}.step("Can you caption the `boat_image`?", boat_image=boat_image)
|
118 |
+
print(boat_image_caption)
|
119 |
+
|
120 |
+
# Text to image -> Unconditional image captioning -> Text to speech
|
121 |
+
boat_audio = {name}.step("Can you generate an image of a boat? Please read out loud the contents of the image afterwards")
|
122 |
+
|
123 |
+
# Text downloading
|
124 |
+
document = {name}.step("Download the text from http://hf.co")
|
125 |
+
print(document)
|
126 |
+
|
127 |
+
# Text summarization
|
128 |
+
summary = {name}.step("Summarize the following text: `document`", document=document)
|
129 |
+
print(summary)
|
130 |
+
|
131 |
+
# Text downloading -> Text summarization -> Text to speech
|
132 |
+
audio = {name}.step("Read out loud the summary of http://hf.co")
|
133 |
+
```
|
134 |
+
|
135 |
+
Chat-based execution (chat), the agent also has a chat-based approach, using the chat() method:
|
136 |
+
```
|
137 |
+
# Clean the chat history
|
138 |
+
{name}.reset()
|
139 |
+
|
140 |
+
# Text to image
|
141 |
+
capybara_image = {name}.chat("Show me an an image of a capybara")
|
142 |
+
capybara_image.save("./capybara_image.png")
|
143 |
+
|
144 |
+
# Image transformation
|
145 |
+
transformed_capybara_image = {name}.chat("Transform the image so that it snows")
|
146 |
+
transformed_capybara_image.save("./transformed_capybara_image.png")
|
147 |
+
|
148 |
+
# Image segmentation
|
149 |
+
segmented_transformed_capybara_image = {name}.chat("Show me a mask of the snowy capybaras")
|
150 |
+
segmented_transformed_capybara_image.save("./segmented_transformed_capybara_image.png")
|
151 |
+
```
|
152 |
+
"""
|
153 |
+
super(HuggingFaceToolAgent, self).__init__(name, description)
|
154 |
+
self.remote = remote
|
155 |
+
|
156 |
+
def reset(self) -> None:
|
157 |
+
r"""Resets the chat history of the agent."""
|
158 |
+
self.agent.prepare_for_new_chat()
|
159 |
+
|
160 |
+
def step(
|
161 |
+
self,
|
162 |
+
*args: Any,
|
163 |
+
remote: Optional[bool] = None,
|
164 |
+
**kwargs: Any,
|
165 |
+
) -> Any:
|
166 |
+
r"""Runs the agent in single execution mode.
|
167 |
+
|
168 |
+
Args:
|
169 |
+
*args (Any): Positional arguments to pass to the agent.
|
170 |
+
remote (bool, optional): Flag indicating whether to run the agent
|
171 |
+
remotely. Overrides the default setting. (default: :obj:`None`)
|
172 |
+
**kwargs (Any): Keyword arguments to pass to the agent.
|
173 |
+
|
174 |
+
Returns:
|
175 |
+
str: The response from the agent.
|
176 |
+
"""
|
177 |
+
if remote is None:
|
178 |
+
remote = self.remote
|
179 |
+
agent_output = self.agent.run(*args, remote=remote, **kwargs)
|
180 |
+
if isinstance(agent_output, self.agent_image_type):
|
181 |
+
agent_output = agent_output.to_raw()
|
182 |
+
return agent_output
|
183 |
+
|
184 |
+
def chat(
|
185 |
+
self,
|
186 |
+
*args: Any,
|
187 |
+
remote: Optional[bool] = None,
|
188 |
+
**kwargs: Any,
|
189 |
+
) -> Any:
|
190 |
+
r"""Runs the agent in a chat conversation mode.
|
191 |
+
|
192 |
+
Args:
|
193 |
+
*args (Any): Positional arguments to pass to the agent.
|
194 |
+
remote (bool, optional): Flag indicating whether to run the agent
|
195 |
+
remotely. Overrides the default setting. (default: :obj:`None`)
|
196 |
+
**kwargs (Any): Keyword arguments to pass to the agent.
|
197 |
+
|
198 |
+
Returns:
|
199 |
+
str: The response from the agent.
|
200 |
+
"""
|
201 |
+
if remote is None:
|
202 |
+
remote = self.remote
|
203 |
+
agent_output = self.agent.chat(*args, remote=remote, **kwargs)
|
204 |
+
if isinstance(agent_output, self.agent_image_type):
|
205 |
+
agent_output = agent_output.to_raw()
|
206 |
+
return agent_output
|
owl/camel/benchmarks/__init__.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .base import BaseBenchmark
|
16 |
+
|
17 |
+
__all__ = ["BaseBenchmark"]
|
owl/camel/benchmarks/base.py
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 logging
|
16 |
+
from abc import ABC, abstractmethod
|
17 |
+
from pathlib import Path
|
18 |
+
from typing import Any, Dict, List, Literal, Optional
|
19 |
+
|
20 |
+
from camel.agents import ChatAgent
|
21 |
+
|
22 |
+
logger = logging.getLogger(__name__)
|
23 |
+
|
24 |
+
|
25 |
+
class BaseBenchmark(ABC):
|
26 |
+
r"""Base class for benchmarks.
|
27 |
+
|
28 |
+
Attributes:
|
29 |
+
name (str): Name of the benchmark.
|
30 |
+
data_dir (str): Path to the data directory.
|
31 |
+
save_to (str): Path to save the results.
|
32 |
+
processes (int): Number of processes to use for parallel
|
33 |
+
processing. :(default: :obj:`1`)
|
34 |
+
"""
|
35 |
+
|
36 |
+
def __init__(
|
37 |
+
self, name: str, data_dir: str, save_to: str, processes: int = 1
|
38 |
+
):
|
39 |
+
r"""Initialize the benchmark.
|
40 |
+
|
41 |
+
Args:
|
42 |
+
name (str): Name of the benchmark.
|
43 |
+
data_dir (str): Path to the data directory.
|
44 |
+
save_to (str): Path to save the results.
|
45 |
+
processes (int): Number of processes to use for parallel
|
46 |
+
processing. :(default: :obj:`1`)
|
47 |
+
|
48 |
+
"""
|
49 |
+
self.name = name
|
50 |
+
self.data_dir = Path(data_dir)
|
51 |
+
self.processes = processes
|
52 |
+
self.save_to = save_to
|
53 |
+
if not self.data_dir.exists():
|
54 |
+
logger.info(
|
55 |
+
f"Data directory {data_dir} does not exist. Creating it."
|
56 |
+
)
|
57 |
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
58 |
+
if not self.data_dir.is_dir():
|
59 |
+
raise NotADirectoryError(
|
60 |
+
f"Data directory {data_dir} is not a directory"
|
61 |
+
)
|
62 |
+
self._data: Dict[str, List[Dict[str, Any]]] = dict()
|
63 |
+
self._results: List[Dict[str, Any]] = []
|
64 |
+
|
65 |
+
@abstractmethod
|
66 |
+
def download(self) -> "BaseBenchmark":
|
67 |
+
r"""Download the benchmark data.
|
68 |
+
|
69 |
+
Returns:
|
70 |
+
BaseBenchmark: The benchmark instance.
|
71 |
+
"""
|
72 |
+
pass
|
73 |
+
|
74 |
+
@abstractmethod
|
75 |
+
def load(self, force_download: bool = False) -> "BaseBenchmark":
|
76 |
+
r"""Load the benchmark data.
|
77 |
+
|
78 |
+
Args:
|
79 |
+
force_download (bool): Whether to force download the data.
|
80 |
+
|
81 |
+
Returns:
|
82 |
+
BaseBenchmark: The benchmark instance.
|
83 |
+
"""
|
84 |
+
pass
|
85 |
+
|
86 |
+
@property
|
87 |
+
def train(self) -> List[Dict[str, Any]]:
|
88 |
+
r"""Get the training data.
|
89 |
+
|
90 |
+
Returns:
|
91 |
+
List[Dict[str, Any]]: The training data.
|
92 |
+
"""
|
93 |
+
if not self._data:
|
94 |
+
logger.info("Data not loaded. Loading data.")
|
95 |
+
self.load()
|
96 |
+
return self._data["train"]
|
97 |
+
|
98 |
+
@property
|
99 |
+
def valid(self) -> List[Dict[str, Any]]:
|
100 |
+
r"""Get the validation data.
|
101 |
+
|
102 |
+
Returns:
|
103 |
+
List[Dict[str, Any]]: The validation data.
|
104 |
+
"""
|
105 |
+
if not self._data:
|
106 |
+
logger.info("Data not loaded. Loading data.")
|
107 |
+
self.load()
|
108 |
+
return self._data["valid"]
|
109 |
+
|
110 |
+
@property
|
111 |
+
def test(self) -> List[Dict[str, Any]]:
|
112 |
+
r"""Get the test data.
|
113 |
+
|
114 |
+
Returns:
|
115 |
+
List[Dict[str, Any]]: The test data.
|
116 |
+
"""
|
117 |
+
if not self._data:
|
118 |
+
logger.info("Data not loaded. Loading data.")
|
119 |
+
self.load()
|
120 |
+
return self._data["test"]
|
121 |
+
|
122 |
+
@abstractmethod
|
123 |
+
def run(
|
124 |
+
self,
|
125 |
+
agent: ChatAgent,
|
126 |
+
on: Literal["train", "valid", "test"],
|
127 |
+
randomize: bool = False,
|
128 |
+
subset: Optional[int] = None,
|
129 |
+
*args,
|
130 |
+
**kwargs,
|
131 |
+
) -> "BaseBenchmark":
|
132 |
+
r"""Run the benchmark.
|
133 |
+
|
134 |
+
Args:
|
135 |
+
agent (ChatAgent): The chat agent.
|
136 |
+
on (str): The data split to run the benchmark on.
|
137 |
+
randomize (bool): Whether to randomize the data.
|
138 |
+
subset (int): The subset of the data to run the benchmark on.
|
139 |
+
|
140 |
+
Returns:
|
141 |
+
BaseBenchmark: The benchmark instance.
|
142 |
+
"""
|
143 |
+
pass
|
144 |
+
|
145 |
+
@property
|
146 |
+
def results(self) -> List[Dict[str, Any]]:
|
147 |
+
r"""Get the results.
|
148 |
+
|
149 |
+
Returns:
|
150 |
+
List[Dict[str, Any]]: The results.
|
151 |
+
"""
|
152 |
+
return self._results
|
owl/camel/bots/__init__.py
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .discord_app import DiscordApp
|
15 |
+
from .slack.models import (
|
16 |
+
SlackAppMentionEventBody,
|
17 |
+
SlackAppMentionEventProfile,
|
18 |
+
SlackAuthProfile,
|
19 |
+
SlackEventBody,
|
20 |
+
SlackEventProfile,
|
21 |
+
)
|
22 |
+
from .slack.slack_app import SlackApp
|
23 |
+
from .telegram_bot import TelegramBot
|
24 |
+
|
25 |
+
__all__ = [
|
26 |
+
'DiscordApp',
|
27 |
+
'SlackApp',
|
28 |
+
'SlackAppMentionEventBody',
|
29 |
+
'SlackAppMentionEventProfile',
|
30 |
+
'SlackAuthProfile',
|
31 |
+
'SlackEventBody',
|
32 |
+
'SlackEventProfile',
|
33 |
+
'TelegramBot',
|
34 |
+
]
|
owl/camel/bots/discord_app.py
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 logging
|
15 |
+
import os
|
16 |
+
from typing import TYPE_CHECKING, List, Optional
|
17 |
+
|
18 |
+
from camel.utils import dependencies_required
|
19 |
+
|
20 |
+
if TYPE_CHECKING:
|
21 |
+
from discord import Message
|
22 |
+
|
23 |
+
logging.basicConfig(level=logging.INFO)
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
|
27 |
+
class DiscordApp:
|
28 |
+
r"""A class representing a Discord app that uses the `discord.py` library
|
29 |
+
to interact with Discord servers.
|
30 |
+
|
31 |
+
This bot can respond to messages in specific channels and only reacts to
|
32 |
+
messages that mention the bot.
|
33 |
+
|
34 |
+
Attributes:
|
35 |
+
channel_ids (Optional[List[int]]): A list of allowed channel IDs. If
|
36 |
+
provided, the bot will only respond to messages in these channels.
|
37 |
+
token (Optional[str]): The Discord bot token used for authentication.
|
38 |
+
"""
|
39 |
+
|
40 |
+
@dependencies_required('discord')
|
41 |
+
def __init__(
|
42 |
+
self,
|
43 |
+
channel_ids: Optional[List[int]] = None,
|
44 |
+
token: Optional[str] = None,
|
45 |
+
) -> None:
|
46 |
+
r"""Initialize the DiscordApp instance by setting up the Discord client
|
47 |
+
and event handlers.
|
48 |
+
|
49 |
+
Args:
|
50 |
+
channel_ids (Optional[List[int]]): A list of allowed channel IDs.
|
51 |
+
The bot will only respond to messages in these channels if
|
52 |
+
provided.
|
53 |
+
token (Optional[str]): The Discord bot token for authentication.
|
54 |
+
If not provided, the token will be retrieved from the
|
55 |
+
environment variable `DISCORD_TOKEN`.
|
56 |
+
|
57 |
+
Raises:
|
58 |
+
ValueError: If the `DISCORD_TOKEN` is not found in environment
|
59 |
+
variables.
|
60 |
+
"""
|
61 |
+
self.token = token or os.getenv('DISCORD_TOKEN')
|
62 |
+
self.channel_ids = channel_ids
|
63 |
+
|
64 |
+
if not self.token:
|
65 |
+
raise ValueError(
|
66 |
+
"`DISCORD_TOKEN` not found in environment variables. Get it"
|
67 |
+
" here: `https://discord.com/developers/applications`."
|
68 |
+
)
|
69 |
+
|
70 |
+
import discord
|
71 |
+
|
72 |
+
intents = discord.Intents.default()
|
73 |
+
intents.message_content = True
|
74 |
+
self._client = discord.Client(intents=intents)
|
75 |
+
|
76 |
+
# Register event handlers
|
77 |
+
self._client.event(self.on_ready)
|
78 |
+
self._client.event(self.on_message)
|
79 |
+
|
80 |
+
async def start(self):
|
81 |
+
r"""Asynchronously start the Discord bot using its token.
|
82 |
+
|
83 |
+
This method starts the bot and logs into Discord asynchronously using
|
84 |
+
the provided token. It should be awaited when used in an async
|
85 |
+
environment.
|
86 |
+
"""
|
87 |
+
await self._client.start(self.token)
|
88 |
+
|
89 |
+
def run(self) -> None:
|
90 |
+
r"""Start the Discord bot using its token.
|
91 |
+
|
92 |
+
This method starts the bot and logs into Discord synchronously using
|
93 |
+
the provided token. It blocks execution and keeps the bot running.
|
94 |
+
"""
|
95 |
+
self._client.run(self.token) # type: ignore[arg-type]
|
96 |
+
|
97 |
+
async def on_ready(self) -> None:
|
98 |
+
r"""Event handler that is called when the bot has successfully
|
99 |
+
connected to the Discord server.
|
100 |
+
|
101 |
+
When the bot is ready and logged into Discord, it prints a message
|
102 |
+
displaying the bot's username.
|
103 |
+
"""
|
104 |
+
logger.info(f'We have logged in as {self._client.user}')
|
105 |
+
|
106 |
+
async def on_message(self, message: 'Message') -> None:
|
107 |
+
r"""Event handler for processing incoming messages.
|
108 |
+
|
109 |
+
This method is called whenever a new message is received by the bot. It
|
110 |
+
will ignore messages sent by the bot itself, only respond to messages
|
111 |
+
in allowed channels (if specified), and only to messages that mention
|
112 |
+
the bot.
|
113 |
+
|
114 |
+
Args:
|
115 |
+
message (discord.Message): The message object received from
|
116 |
+
Discord.
|
117 |
+
"""
|
118 |
+
# If the message author is the bot itself,
|
119 |
+
# do not respond to this message
|
120 |
+
if message.author == self._client.user:
|
121 |
+
return
|
122 |
+
|
123 |
+
# If allowed channel IDs are provided,
|
124 |
+
# only respond to messages in those channels
|
125 |
+
if self.channel_ids and message.channel.id not in self.channel_ids:
|
126 |
+
return
|
127 |
+
|
128 |
+
# Only respond to messages that mention the bot
|
129 |
+
if not self._client.user or not self._client.user.mentioned_in(
|
130 |
+
message
|
131 |
+
):
|
132 |
+
return
|
133 |
+
|
134 |
+
logger.info(f"Received message: {message.content}")
|
135 |
+
|
136 |
+
@property
|
137 |
+
def client(self):
|
138 |
+
return self._client
|
owl/camel/bots/slack/__init__.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .models import (
|
15 |
+
SlackAppMentionEventBody,
|
16 |
+
SlackAppMentionEventProfile,
|
17 |
+
SlackAuthProfile,
|
18 |
+
SlackEventBody,
|
19 |
+
SlackEventProfile,
|
20 |
+
)
|
21 |
+
from .slack_app import SlackApp
|
22 |
+
|
23 |
+
__all__ = [
|
24 |
+
'SlackApp',
|
25 |
+
'SlackAppMentionEventBody',
|
26 |
+
'SlackAppMentionEventProfile',
|
27 |
+
'SlackAuthProfile',
|
28 |
+
'SlackEventBody',
|
29 |
+
'SlackEventProfile',
|
30 |
+
]
|
owl/camel/bots/slack/models.py
ADDED
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from typing import Optional
|
15 |
+
|
16 |
+
from pydantic import BaseModel
|
17 |
+
|
18 |
+
|
19 |
+
class SlackAuthProfile(BaseModel):
|
20 |
+
r"""Represents the authorization profile within a Slack event.
|
21 |
+
|
22 |
+
Events will contain a single, compact authorizations field that shows one
|
23 |
+
installation of your app that the event is visible to.
|
24 |
+
In other words, lists of authorizations will be truncated to one element.
|
25 |
+
|
26 |
+
If there's more than one installing party that your app is keeping track
|
27 |
+
of, it's best not to rely on the single party listed in authorizations to
|
28 |
+
be any particular one.
|
29 |
+
|
30 |
+
To get a full list of who can see events, call the apps.event.
|
31 |
+
authorizations.list method after obtaining an app-level token. Read more on
|
32 |
+
the changes here; they have taken effect for existing apps as of
|
33 |
+
February 24, 2021.
|
34 |
+
|
35 |
+
References:
|
36 |
+
|
37 |
+
- https://api.slack.com/apis/events-api#authorizations
|
38 |
+
- https://api.slack.com/changelog/2020-09-15-events-api-truncate-authed-users#no_context
|
39 |
+
"""
|
40 |
+
|
41 |
+
enterprise_id: Optional[str] = None
|
42 |
+
"""The ID of the enterprise associated with the authorization."""
|
43 |
+
|
44 |
+
team_id: str
|
45 |
+
"""The ID of the team associated with the authorization."""
|
46 |
+
|
47 |
+
user_id: str
|
48 |
+
"""The ID of the user associated with the authorization."""
|
49 |
+
|
50 |
+
is_bot: bool
|
51 |
+
"""Whether the authorized user is a bot."""
|
52 |
+
|
53 |
+
is_enterprise_install: bool
|
54 |
+
"""Whether the authorization is for an enterprise installation."""
|
55 |
+
|
56 |
+
|
57 |
+
class SlackEventProfile(BaseModel):
|
58 |
+
r"""Represents the detailed profile of a Slack event, including user,
|
59 |
+
message, and context data.
|
60 |
+
"""
|
61 |
+
|
62 |
+
user: str
|
63 |
+
"""The ID of the user associated with the event."""
|
64 |
+
|
65 |
+
type: str
|
66 |
+
"""The type of the event (e.g., 'message')."""
|
67 |
+
|
68 |
+
ts: str
|
69 |
+
"""A timestamp representing when the event was triggered."""
|
70 |
+
|
71 |
+
thread_ts: Optional[str] = None
|
72 |
+
"""The timestamp of the parent message in a thread."""
|
73 |
+
|
74 |
+
client_msg_id: str
|
75 |
+
"""A unique ID generated by the client for the message (if available)."""
|
76 |
+
|
77 |
+
text: str
|
78 |
+
"""The message content text."""
|
79 |
+
|
80 |
+
team: str
|
81 |
+
"""The ID of the team that the event is associated with."""
|
82 |
+
|
83 |
+
blocks: list
|
84 |
+
"""The list of message blocks, providing structured information."""
|
85 |
+
|
86 |
+
channel: str
|
87 |
+
"""The ID of the Slack channel where the event happened."""
|
88 |
+
|
89 |
+
event_ts: str
|
90 |
+
"""The event-specific timestamp when it occurred."""
|
91 |
+
|
92 |
+
channel_type: Optional[str]
|
93 |
+
"""The type of Slack channel (e.g., 'channel', 'im')."""
|
94 |
+
|
95 |
+
|
96 |
+
class SlackEventBody(BaseModel):
|
97 |
+
r"""Represents the entire body of a Slack event, including the event
|
98 |
+
profile, authorization, and context.
|
99 |
+
"""
|
100 |
+
|
101 |
+
token: str
|
102 |
+
"""The token to verify the source of the event."""
|
103 |
+
|
104 |
+
team_id: str
|
105 |
+
"""The ID of the team where the event is happening."""
|
106 |
+
|
107 |
+
context_team_id: Optional[str]
|
108 |
+
"""The team ID for the shared channel context, if applicable."""
|
109 |
+
|
110 |
+
context_enterprise_id: Optional[str] = None
|
111 |
+
"""The enterprise ID for the shared channel context, if applicable."""
|
112 |
+
|
113 |
+
api_app_id: str
|
114 |
+
"""The unique identifier for the Slack app that received the event."""
|
115 |
+
|
116 |
+
event: SlackEventProfile
|
117 |
+
"""A detailed profile of the event"""
|
118 |
+
|
119 |
+
type: str
|
120 |
+
"""The overall type of event received (e.g., 'event_callback')."""
|
121 |
+
|
122 |
+
event_id: str
|
123 |
+
"""A unique identifier assigned to this event by Slack."""
|
124 |
+
|
125 |
+
event_time: int
|
126 |
+
"""The timestamp (in seconds) representing when the event was triggered."""
|
127 |
+
|
128 |
+
authorizations: Optional[list[SlackAuthProfile]] = None
|
129 |
+
"""An optional list of authorizations that describe which installation can
|
130 |
+
see the event."""
|
131 |
+
|
132 |
+
is_ext_shared_channel: bool
|
133 |
+
"""Indicates if the event is part of a shared channel between different
|
134 |
+
organizations."""
|
135 |
+
|
136 |
+
event_context: str
|
137 |
+
"""A unique string representing the context of the event."""
|
138 |
+
|
139 |
+
|
140 |
+
class SlackAppMentionEventProfile(SlackEventProfile):
|
141 |
+
r"""Represents the detailed profile of a Slack event where the app was
|
142 |
+
mentioned in a message.
|
143 |
+
"""
|
144 |
+
|
145 |
+
channel_type: Optional[str] = None
|
146 |
+
"""The type of Slack channel. it's None for app mentions."""
|
147 |
+
|
148 |
+
|
149 |
+
class SlackAppMentionEventBody(SlackEventBody):
|
150 |
+
r"""Represents the entire body of a Slack event where the app was mentioned
|
151 |
+
in a message.
|
152 |
+
"""
|
153 |
+
|
154 |
+
context_team_id: Optional[str] = None
|
155 |
+
"""A detailed profile of the event. it's None for app mentions."""
|
156 |
+
|
157 |
+
event: SlackAppMentionEventProfile
|
158 |
+
"""A detailed profile of the event"""
|
owl/camel/bots/slack/slack_app.py
ADDED
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 logging
|
15 |
+
import os
|
16 |
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
17 |
+
|
18 |
+
from slack_sdk.oauth.installation_store.async_installation_store import (
|
19 |
+
AsyncInstallationStore,
|
20 |
+
)
|
21 |
+
from starlette import requests, responses
|
22 |
+
|
23 |
+
from camel.bots.slack.models import (
|
24 |
+
SlackAppMentionEventBody,
|
25 |
+
SlackAppMentionEventProfile,
|
26 |
+
SlackEventBody,
|
27 |
+
SlackEventProfile,
|
28 |
+
)
|
29 |
+
from camel.utils import dependencies_required
|
30 |
+
|
31 |
+
if TYPE_CHECKING:
|
32 |
+
from slack_bolt.context.async_context import AsyncBoltContext
|
33 |
+
from slack_bolt.context.say.async_say import AsyncSay
|
34 |
+
from slack_sdk.web.async_client import AsyncWebClient
|
35 |
+
|
36 |
+
logging.basicConfig(level=logging.INFO)
|
37 |
+
logger = logging.getLogger(__name__)
|
38 |
+
|
39 |
+
|
40 |
+
class SlackApp:
|
41 |
+
r"""Represents a Slack app that is powered by a Slack Bolt `AsyncApp`.
|
42 |
+
|
43 |
+
This class is responsible for initializing and managing the Slack
|
44 |
+
application by setting up event handlers, running the app server, and
|
45 |
+
handling events such as messages and mentions from Slack.
|
46 |
+
|
47 |
+
Args:
|
48 |
+
token (Optional[str]): Slack API token for authentication.
|
49 |
+
scopes (Optional[str]): Slack app scopes for permissions.
|
50 |
+
signing_secret (Optional[str]): Signing secret for verifying Slack
|
51 |
+
requests.
|
52 |
+
client_id (Optional[str]): Slack app client ID.
|
53 |
+
client_secret (Optional[str]): Slack app client secret.
|
54 |
+
redirect_uri_path (str): The URI path for OAuth redirect, defaults to
|
55 |
+
"/slack/oauth_redirect".
|
56 |
+
installation_store (Optional[AsyncInstallationStore]): The installation
|
57 |
+
store for handling OAuth installations.
|
58 |
+
"""
|
59 |
+
|
60 |
+
@dependencies_required('slack_bolt')
|
61 |
+
def __init__(
|
62 |
+
self,
|
63 |
+
token: Optional[str] = None,
|
64 |
+
scopes: Optional[str] = None,
|
65 |
+
signing_secret: Optional[str] = None,
|
66 |
+
client_id: Optional[str] = None,
|
67 |
+
client_secret: Optional[str] = None,
|
68 |
+
redirect_uri_path: str = "/slack/oauth_redirect",
|
69 |
+
installation_store: Optional[AsyncInstallationStore] = None,
|
70 |
+
) -> None:
|
71 |
+
r"""Initializes the SlackApp instance by setting up the Slack Bolt app
|
72 |
+
and configuring event handlers and OAuth settings.
|
73 |
+
|
74 |
+
Args:
|
75 |
+
token (Optional[str]): The Slack API token.
|
76 |
+
scopes (Optional[str]): The scopes for Slack app permissions.
|
77 |
+
signing_secret (Optional[str]): The signing secret for verifying
|
78 |
+
requests.
|
79 |
+
client_id (Optional[str]): The Slack app client ID.
|
80 |
+
client_secret (Optional[str]): The Slack app client secret.
|
81 |
+
redirect_uri_path (str): The URI path for handling OAuth redirects
|
82 |
+
(default is "/slack/oauth_redirect").
|
83 |
+
installation_store (Optional[AsyncInstallationStore]): An optional
|
84 |
+
installation store for OAuth installations.
|
85 |
+
"""
|
86 |
+
from slack_bolt.adapter.starlette.async_handler import (
|
87 |
+
AsyncSlackRequestHandler,
|
88 |
+
)
|
89 |
+
from slack_bolt.app.async_app import AsyncApp
|
90 |
+
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
|
91 |
+
|
92 |
+
self.token: Optional[str] = token or os.getenv("SLACK_TOKEN")
|
93 |
+
self.scopes: Optional[str] = scopes or os.getenv("SLACK_SCOPES")
|
94 |
+
self.signing_secret: Optional[str] = signing_secret or os.getenv(
|
95 |
+
"SLACK_SIGNING_SECRET"
|
96 |
+
)
|
97 |
+
self.client_id: Optional[str] = client_id or os.getenv(
|
98 |
+
"SLACK_CLIENT_ID"
|
99 |
+
)
|
100 |
+
self.client_secret: Optional[str] = client_secret or os.getenv(
|
101 |
+
"SLACK_CLIENT_SECRET"
|
102 |
+
)
|
103 |
+
|
104 |
+
if not all([self.token, self.scopes, self.signing_secret]):
|
105 |
+
raise ValueError(
|
106 |
+
"`SLACK_TOKEN`, `SLACK_SCOPES`, and `SLACK_SIGNING_SECRET` "
|
107 |
+
"environment variables must be set. Get it here: "
|
108 |
+
"`https://api.slack.com/apps`."
|
109 |
+
)
|
110 |
+
|
111 |
+
# Setup OAuth settings if client ID and secret are provided
|
112 |
+
if self.client_id and self.client_secret:
|
113 |
+
self._app = AsyncApp(
|
114 |
+
oauth_settings=AsyncOAuthSettings(
|
115 |
+
client_id=self.client_id,
|
116 |
+
client_secret=self.client_secret,
|
117 |
+
scopes=self.scopes,
|
118 |
+
redirect_uri_path=redirect_uri_path,
|
119 |
+
),
|
120 |
+
logger=logger,
|
121 |
+
signing_secret=self.signing_secret,
|
122 |
+
installation_store=installation_store,
|
123 |
+
token=self.token,
|
124 |
+
)
|
125 |
+
else:
|
126 |
+
# Initialize Slack Bolt AsyncApp with settings
|
127 |
+
self._app = AsyncApp(
|
128 |
+
logger=logger,
|
129 |
+
signing_secret=self.signing_secret,
|
130 |
+
installation_store=installation_store,
|
131 |
+
token=self.token,
|
132 |
+
)
|
133 |
+
|
134 |
+
self._handler = AsyncSlackRequestHandler(self._app)
|
135 |
+
self.setup_handlers()
|
136 |
+
|
137 |
+
def setup_handlers(self) -> None:
|
138 |
+
r"""Sets up the event handlers for Slack events, such as `app_mention`
|
139 |
+
and `message`.
|
140 |
+
|
141 |
+
This method registers the `app_mention` and `on_message` event handlers
|
142 |
+
with the Slack Bolt app to respond to Slack events.
|
143 |
+
"""
|
144 |
+
self._app.event("app_mention")(self.app_mention)
|
145 |
+
self._app.event("message")(self.on_message)
|
146 |
+
|
147 |
+
def run(
|
148 |
+
self,
|
149 |
+
port: int = 3000,
|
150 |
+
path: str = "/slack/events",
|
151 |
+
host: Optional[str] = None,
|
152 |
+
) -> None:
|
153 |
+
r"""Starts the Slack Bolt app server to listen for incoming Slack
|
154 |
+
events.
|
155 |
+
|
156 |
+
Args:
|
157 |
+
port (int): The port on which the server should run (default is
|
158 |
+
3000).
|
159 |
+
path (str): The endpoint path for receiving Slack events (default
|
160 |
+
is "/slack/events").
|
161 |
+
host (Optional[str]): The hostname to bind the server (default is
|
162 |
+
None).
|
163 |
+
"""
|
164 |
+
self._app.start(port=port, path=path, host=host)
|
165 |
+
|
166 |
+
async def handle_request(
|
167 |
+
self, request: requests.Request
|
168 |
+
) -> responses.Response:
|
169 |
+
r"""Handles incoming requests from Slack through the request handler.
|
170 |
+
|
171 |
+
Args:
|
172 |
+
request (Request): A Starlette request object representing the
|
173 |
+
incoming request.
|
174 |
+
|
175 |
+
Returns:
|
176 |
+
The response generated by the Slack Bolt handler.
|
177 |
+
"""
|
178 |
+
return await self._handler.handle(request)
|
179 |
+
|
180 |
+
async def app_mention(
|
181 |
+
self,
|
182 |
+
context: "AsyncBoltContext",
|
183 |
+
client: "AsyncWebClient",
|
184 |
+
event: Dict[str, Any],
|
185 |
+
body: Dict[str, Any],
|
186 |
+
say: "AsyncSay",
|
187 |
+
) -> None:
|
188 |
+
r"""Event handler for `app_mention` events.
|
189 |
+
|
190 |
+
This method is triggered when someone mentions the app in Slack.
|
191 |
+
|
192 |
+
Args:
|
193 |
+
context (AsyncBoltContext): The Slack Bolt context for the event.
|
194 |
+
client (AsyncWebClient): The Slack Web API client.
|
195 |
+
event (Dict[str, Any]): The event data for the app mention.
|
196 |
+
body (Dict[str, Any]): The full request body from Slack.
|
197 |
+
say (AsyncSay): A function to send a response back to the channel.
|
198 |
+
"""
|
199 |
+
event_profile = SlackAppMentionEventProfile(**event)
|
200 |
+
event_body = SlackAppMentionEventBody(**body)
|
201 |
+
|
202 |
+
logger.info(f"app_mention, context: {context}")
|
203 |
+
logger.info(f"app_mention, client: {client}")
|
204 |
+
logger.info(f"app_mention, event_profile: {event_profile}")
|
205 |
+
logger.info(f"app_mention, event_body: {event_body}")
|
206 |
+
logger.info(f"app_mention, say: {say}")
|
207 |
+
|
208 |
+
async def on_message(
|
209 |
+
self,
|
210 |
+
context: "AsyncBoltContext",
|
211 |
+
client: "AsyncWebClient",
|
212 |
+
event: Dict[str, Any],
|
213 |
+
body: Dict[str, Any],
|
214 |
+
say: "AsyncSay",
|
215 |
+
) -> None:
|
216 |
+
r"""Event handler for `message` events.
|
217 |
+
|
218 |
+
This method is triggered when the app receives a message in Slack.
|
219 |
+
|
220 |
+
Args:
|
221 |
+
context (AsyncBoltContext): The Slack Bolt context for the event.
|
222 |
+
client (AsyncWebClient): The Slack Web API client.
|
223 |
+
event (Dict[str, Any]): The event data for the message.
|
224 |
+
body (Dict[str, Any]): The full request body from Slack.
|
225 |
+
say (AsyncSay): A function to send a response back to the channel.
|
226 |
+
"""
|
227 |
+
await context.ack()
|
228 |
+
|
229 |
+
event_profile = SlackEventProfile(**event)
|
230 |
+
event_body = SlackEventBody(**body)
|
231 |
+
|
232 |
+
logger.info(f"on_message, context: {context}")
|
233 |
+
logger.info(f"on_message, client: {client}")
|
234 |
+
logger.info(f"on_message, event_profile: {event_profile}")
|
235 |
+
logger.info(f"on_message, event_body: {event_body}")
|
236 |
+
logger.info(f"on_message, say: {say}")
|
237 |
+
|
238 |
+
logger.info(f"Received message: {event_profile.text}")
|
239 |
+
|
240 |
+
def mention_me(
|
241 |
+
self, context: "AsyncBoltContext", body: SlackEventBody
|
242 |
+
) -> bool:
|
243 |
+
r"""Check if the bot is mentioned in the message.
|
244 |
+
|
245 |
+
Args:
|
246 |
+
context (AsyncBoltContext): The Slack Bolt context for the event.
|
247 |
+
body (SlackEventBody): The body of the Slack event.
|
248 |
+
|
249 |
+
Returns:
|
250 |
+
bool: True if the bot is mentioned in the message, False otherwise.
|
251 |
+
"""
|
252 |
+
message = body.event.text
|
253 |
+
bot_user_id = context.bot_user_id
|
254 |
+
mention = f"<@{bot_user_id}>"
|
255 |
+
return mention in message
|
owl/camel/bots/telegram_bot.py
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 os
|
15 |
+
from typing import TYPE_CHECKING, Optional
|
16 |
+
|
17 |
+
from camel.agents import ChatAgent
|
18 |
+
from camel.messages import BaseMessage
|
19 |
+
from camel.utils import dependencies_required
|
20 |
+
|
21 |
+
# Conditionally import telebot types only for type checking
|
22 |
+
if TYPE_CHECKING:
|
23 |
+
from telebot.types import ( # type: ignore[import-untyped]
|
24 |
+
Message,
|
25 |
+
)
|
26 |
+
|
27 |
+
|
28 |
+
class TelegramBot:
|
29 |
+
r"""Represents a Telegram bot that is powered by an agent.
|
30 |
+
|
31 |
+
Attributes:
|
32 |
+
chat_agent (ChatAgent): Chat agent that will power the bot.
|
33 |
+
telegram_token (str, optional): The bot token.
|
34 |
+
"""
|
35 |
+
|
36 |
+
@dependencies_required('telebot')
|
37 |
+
def __init__(
|
38 |
+
self,
|
39 |
+
chat_agent: ChatAgent,
|
40 |
+
telegram_token: Optional[str] = None,
|
41 |
+
) -> None:
|
42 |
+
self.chat_agent = chat_agent
|
43 |
+
|
44 |
+
if not telegram_token:
|
45 |
+
self.token = os.getenv('TELEGRAM_TOKEN')
|
46 |
+
if not self.token:
|
47 |
+
raise ValueError(
|
48 |
+
"`TELEGRAM_TOKEN` not found in environment variables. "
|
49 |
+
"Get it from t.me/BotFather."
|
50 |
+
)
|
51 |
+
else:
|
52 |
+
self.token = telegram_token
|
53 |
+
|
54 |
+
import telebot # type: ignore[import-untyped]
|
55 |
+
|
56 |
+
self.bot = telebot.TeleBot(token=self.token)
|
57 |
+
|
58 |
+
# Register the message handler within the constructor
|
59 |
+
self.bot.message_handler(func=lambda message: True)(self.on_message)
|
60 |
+
|
61 |
+
def run(self) -> None:
|
62 |
+
r"""Start the Telegram bot."""
|
63 |
+
print("Telegram bot is running...")
|
64 |
+
self.bot.infinity_polling()
|
65 |
+
|
66 |
+
def on_message(self, message: 'Message') -> None:
|
67 |
+
r"""Handles incoming messages from the user.
|
68 |
+
|
69 |
+
Args:
|
70 |
+
message (types.Message): The incoming message object.
|
71 |
+
"""
|
72 |
+
self.chat_agent.reset()
|
73 |
+
|
74 |
+
if not message.text:
|
75 |
+
return
|
76 |
+
|
77 |
+
user_msg = BaseMessage.make_user_message(
|
78 |
+
role_name="User", content=message.text
|
79 |
+
)
|
80 |
+
assistant_response = self.chat_agent.step(user_msg)
|
81 |
+
|
82 |
+
self.bot.reply_to(message, assistant_response.msg.content)
|
owl/camel/configs/__init__.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from .anthropic_config import ANTHROPIC_API_PARAMS, AnthropicConfig
|
15 |
+
from .base_config import BaseConfig
|
16 |
+
from .cohere_config import COHERE_API_PARAMS, CohereConfig
|
17 |
+
from .deepseek_config import DEEPSEEK_API_PARAMS, DeepSeekConfig
|
18 |
+
from .gemini_config import Gemini_API_PARAMS, GeminiConfig
|
19 |
+
from .groq_config import GROQ_API_PARAMS, GroqConfig
|
20 |
+
from .litellm_config import LITELLM_API_PARAMS, LiteLLMConfig
|
21 |
+
from .mistral_config import MISTRAL_API_PARAMS, MistralConfig
|
22 |
+
from .nvidia_config import NVIDIA_API_PARAMS, NvidiaConfig
|
23 |
+
from .ollama_config import OLLAMA_API_PARAMS, OllamaConfig
|
24 |
+
from .openai_config import OPENAI_API_PARAMS, ChatGPTConfig
|
25 |
+
from .qwen_config import QWEN_API_PARAMS, QwenConfig
|
26 |
+
from .reka_config import REKA_API_PARAMS, RekaConfig
|
27 |
+
from .samba_config import (
|
28 |
+
SAMBA_CLOUD_API_PARAMS,
|
29 |
+
SAMBA_VERSE_API_PARAMS,
|
30 |
+
SambaCloudAPIConfig,
|
31 |
+
SambaVerseAPIConfig,
|
32 |
+
)
|
33 |
+
from .togetherai_config import TOGETHERAI_API_PARAMS, TogetherAIConfig
|
34 |
+
from .vllm_config import VLLM_API_PARAMS, VLLMConfig
|
35 |
+
from .yi_config import YI_API_PARAMS, YiConfig
|
36 |
+
from .zhipuai_config import ZHIPUAI_API_PARAMS, ZhipuAIConfig
|
37 |
+
|
38 |
+
__all__ = [
|
39 |
+
'BaseConfig',
|
40 |
+
'ChatGPTConfig',
|
41 |
+
'OPENAI_API_PARAMS',
|
42 |
+
'AnthropicConfig',
|
43 |
+
'ANTHROPIC_API_PARAMS',
|
44 |
+
'GROQ_API_PARAMS',
|
45 |
+
'GroqConfig',
|
46 |
+
'LiteLLMConfig',
|
47 |
+
'LITELLM_API_PARAMS',
|
48 |
+
'NvidiaConfig',
|
49 |
+
'NVIDIA_API_PARAMS',
|
50 |
+
'OllamaConfig',
|
51 |
+
'OLLAMA_API_PARAMS',
|
52 |
+
'ZhipuAIConfig',
|
53 |
+
'ZHIPUAI_API_PARAMS',
|
54 |
+
'GeminiConfig',
|
55 |
+
'Gemini_API_PARAMS',
|
56 |
+
'VLLMConfig',
|
57 |
+
'VLLM_API_PARAMS',
|
58 |
+
'MistralConfig',
|
59 |
+
'MISTRAL_API_PARAMS',
|
60 |
+
'RekaConfig',
|
61 |
+
'REKA_API_PARAMS',
|
62 |
+
'SambaVerseAPIConfig',
|
63 |
+
'SAMBA_VERSE_API_PARAMS',
|
64 |
+
'SambaCloudAPIConfig',
|
65 |
+
'SAMBA_CLOUD_API_PARAMS',
|
66 |
+
'TogetherAIConfig',
|
67 |
+
'TOGETHERAI_API_PARAMS',
|
68 |
+
'CohereConfig',
|
69 |
+
'COHERE_API_PARAMS',
|
70 |
+
'YiConfig',
|
71 |
+
'YI_API_PARAMS',
|
72 |
+
'QwenConfig',
|
73 |
+
'QWEN_API_PARAMS',
|
74 |
+
'DeepSeekConfig',
|
75 |
+
'DEEPSEEK_API_PARAMS',
|
76 |
+
]
|