adam-community 0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adam_community-0.2/PKG-INFO +53 -0
- adam_community-0.2/README.md +27 -0
- adam_community-0.2/adam_community/__init__.py +4 -0
- adam_community-0.2/adam_community/cli/__init__.py +0 -0
- adam_community-0.2/adam_community/cli/build.py +236 -0
- adam_community-0.2/adam_community/cli/cli.py +44 -0
- adam_community-0.2/adam_community/cli/parser.py +121 -0
- adam_community-0.2/adam_community/tool.py +143 -0
- adam_community-0.2/adam_community/util.py +345 -0
- adam_community-0.2/adam_community.egg-info/PKG-INFO +53 -0
- adam_community-0.2/adam_community.egg-info/SOURCES.txt +16 -0
- adam_community-0.2/adam_community.egg-info/dependency_links.txt +1 -0
- adam_community-0.2/adam_community.egg-info/entry_points.txt +2 -0
- adam_community-0.2/adam_community.egg-info/requires.txt +4 -0
- adam_community-0.2/adam_community.egg-info/top_level.txt +1 -0
- adam_community-0.2/setup.cfg +4 -0
- adam_community-0.2/setup.py +31 -0
- adam_community-0.2/test/test_util_tool.py +143 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adam_community
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: Adam Community Tools and Utilities
|
|
5
|
+
Home-page: https://github.com/yourusername/adam-community
|
|
6
|
+
Author: Adam Community
|
|
7
|
+
Author-email: admin@sidereus-ai.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: click>=8.0.0
|
|
15
|
+
Requires-Dist: docstring-parser>=0.15
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# Adam Community
|
|
28
|
+
|
|
29
|
+
Adam Community 是一个 Python 工具包,提供了一系列实用工具和功能,用于与 Adam 社区相关的开发工作。
|
|
30
|
+
|
|
31
|
+
安装依赖:
|
|
32
|
+
```bash
|
|
33
|
+
make install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## 使用方法
|
|
38
|
+
|
|
39
|
+
安装完成后,您可以在 Python 代码中导入并使用该包:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from adam_community import tool
|
|
43
|
+
from adam_community import util
|
|
44
|
+
|
|
45
|
+
# 使用相关功能
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 开发
|
|
49
|
+
|
|
50
|
+
运行测试:
|
|
51
|
+
```bash
|
|
52
|
+
make test
|
|
53
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Adam Community
|
|
2
|
+
|
|
3
|
+
Adam Community 是一个 Python 工具包,提供了一系列实用工具和功能,用于与 Adam 社区相关的开发工作。
|
|
4
|
+
|
|
5
|
+
安装依赖:
|
|
6
|
+
```bash
|
|
7
|
+
make install
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 使用方法
|
|
12
|
+
|
|
13
|
+
安装完成后,您可以在 Python 代码中导入并使用该包:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from adam_community import tool
|
|
17
|
+
from adam_community import util
|
|
18
|
+
|
|
19
|
+
# 使用相关功能
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 开发
|
|
23
|
+
|
|
24
|
+
运行测试:
|
|
25
|
+
```bash
|
|
26
|
+
make test
|
|
27
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import zipfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Tuple, List
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.tree import Tree
|
|
8
|
+
from .parser import parse_directory
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def check_python_files(directory: Path) -> Tuple[bool, List[str]]:
|
|
13
|
+
"""检查所有 Python 文件是否都有参数定义"""
|
|
14
|
+
tree = Tree("📦 Python 文件检查")
|
|
15
|
+
errors = []
|
|
16
|
+
functions = parse_directory(directory)
|
|
17
|
+
|
|
18
|
+
tree.add(f"找到 {len(functions)} 个类定义")
|
|
19
|
+
for func in functions:
|
|
20
|
+
func_info = func["function"]
|
|
21
|
+
if not func_info["description"]:
|
|
22
|
+
tree.add(f"⚠️ {func_info['file']}: {func_info['name']} 没有描述")
|
|
23
|
+
|
|
24
|
+
if not func_info["parameters"]["properties"]:
|
|
25
|
+
tree.add(f"⚠️ {func_info['file']}: {func_info['name']} 没有参数定义")
|
|
26
|
+
else:
|
|
27
|
+
tree.add(f"✓ {func_info['file']}: {func_info['name']}")
|
|
28
|
+
|
|
29
|
+
if errors:
|
|
30
|
+
tree.add("❌ 检查未通过")
|
|
31
|
+
else:
|
|
32
|
+
tree.add("✅ 检查通过")
|
|
33
|
+
|
|
34
|
+
console.print(tree)
|
|
35
|
+
return len(errors) == 0, errors
|
|
36
|
+
|
|
37
|
+
def check_configuration(directory: Path) -> Tuple[bool, List[str]]:
|
|
38
|
+
"""检查 configure.json 文件"""
|
|
39
|
+
tree = Tree("📄 配置文件检查")
|
|
40
|
+
errors = []
|
|
41
|
+
config_path = directory / "config" / "configure.json"
|
|
42
|
+
|
|
43
|
+
if not config_path.exists():
|
|
44
|
+
tree.add("❌ 未找到 configure.json 文件")
|
|
45
|
+
console.print(tree)
|
|
46
|
+
return False, ["configure.json 文件不存在"]
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
50
|
+
config = json.load(f)
|
|
51
|
+
|
|
52
|
+
required_fields = ["name", "version", "display_name"]
|
|
53
|
+
for field in required_fields:
|
|
54
|
+
if field not in config:
|
|
55
|
+
errors.append(f"configure.json 缺少必要字段: {field}")
|
|
56
|
+
tree.add(f"❌ 缺少字段: {field}")
|
|
57
|
+
else:
|
|
58
|
+
tree.add(f"✓ {field}: {config[field]}")
|
|
59
|
+
|
|
60
|
+
if errors:
|
|
61
|
+
tree.add("❌ 检查未通过")
|
|
62
|
+
else:
|
|
63
|
+
tree.add("✅ 检查通过")
|
|
64
|
+
|
|
65
|
+
console.print(tree)
|
|
66
|
+
return len(errors) == 0, errors
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
tree.add("❌ 配置文件格式错误")
|
|
69
|
+
console.print(tree)
|
|
70
|
+
return False, ["configure.json 文件格式错误"]
|
|
71
|
+
|
|
72
|
+
def check_markdown_files(directory: Path) -> Tuple[bool, List[str]]:
|
|
73
|
+
"""检查必要的 Markdown 文件"""
|
|
74
|
+
tree = Tree("📑 Markdown 文件检查")
|
|
75
|
+
errors = []
|
|
76
|
+
|
|
77
|
+
# 先读取配置文件中的 type 字段
|
|
78
|
+
config_path = directory / "config" / "configure.json"
|
|
79
|
+
config_type = "agent" # 默认值
|
|
80
|
+
|
|
81
|
+
if config_path.exists():
|
|
82
|
+
try:
|
|
83
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
84
|
+
config = json.load(f)
|
|
85
|
+
config_type = config.get("type", "agent")
|
|
86
|
+
except json.JSONDecodeError:
|
|
87
|
+
pass # 配置文件格式错误时使用默认值
|
|
88
|
+
|
|
89
|
+
# 根据 type 设置不同的 required_files
|
|
90
|
+
if config_type == "kit":
|
|
91
|
+
required_files = [
|
|
92
|
+
"configure.json",
|
|
93
|
+
"long_description.md",
|
|
94
|
+
"input.json"
|
|
95
|
+
]
|
|
96
|
+
else: # type=agent 或空值
|
|
97
|
+
required_files = [
|
|
98
|
+
"initial_assistant_message.md",
|
|
99
|
+
"initial_system_prompt.md",
|
|
100
|
+
"long_description.md"
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
tree.add(f"检查类型: {config_type}")
|
|
104
|
+
|
|
105
|
+
for file in required_files:
|
|
106
|
+
file_path = directory / "config" / file
|
|
107
|
+
if not file_path.exists():
|
|
108
|
+
errors.append(f"缺少必要文件: {file}")
|
|
109
|
+
tree.add(f"❌ {file}")
|
|
110
|
+
else:
|
|
111
|
+
# 对于 input.json,额外检查 JSON 格式
|
|
112
|
+
if file == "input.json":
|
|
113
|
+
try:
|
|
114
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
115
|
+
json.load(f)
|
|
116
|
+
tree.add(f"✓ {file} (JSON 格式正确)")
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
errors.append(f"{file} JSON 格式错误")
|
|
119
|
+
tree.add(f"❌ {file} (JSON 格式错误)")
|
|
120
|
+
else:
|
|
121
|
+
tree.add(f"✓ {file}")
|
|
122
|
+
|
|
123
|
+
if errors:
|
|
124
|
+
tree.add("❌ 检查未通过")
|
|
125
|
+
else:
|
|
126
|
+
tree.add("✅ 检查通过")
|
|
127
|
+
|
|
128
|
+
console.print(tree)
|
|
129
|
+
return len(errors) == 0, errors
|
|
130
|
+
|
|
131
|
+
def create_zip_package(directory: Path) -> str:
|
|
132
|
+
"""创建 zip 包"""
|
|
133
|
+
tree = Tree("📦 创建压缩包")
|
|
134
|
+
with open(directory / "config" / "configure.json", 'r', encoding='utf-8') as f:
|
|
135
|
+
config = json.load(f)
|
|
136
|
+
|
|
137
|
+
zip_name = f"{config['name']}_{config['version']}.zip"
|
|
138
|
+
zip_path = directory / zip_name
|
|
139
|
+
|
|
140
|
+
tree.add(f"包名: {zip_name}")
|
|
141
|
+
|
|
142
|
+
# 获取配置类型
|
|
143
|
+
config_type = config.get("type", "agent")
|
|
144
|
+
|
|
145
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
146
|
+
# 添加所有 Python 文件
|
|
147
|
+
py_files = list(directory.rglob('*.py'))
|
|
148
|
+
py_tree = tree.add("Python 文件")
|
|
149
|
+
for py_file in py_files:
|
|
150
|
+
if not py_file.name.startswith('_'):
|
|
151
|
+
zipf.write(py_file, py_file.relative_to(directory))
|
|
152
|
+
py_tree.add(f"+ {py_file.relative_to(directory)}")
|
|
153
|
+
|
|
154
|
+
# 添加配置文件
|
|
155
|
+
config_tree = tree.add("配置文件")
|
|
156
|
+
zipf.write(directory / "config" / "configure.json", "config/configure.json")
|
|
157
|
+
config_tree.add("+ config/configure.json")
|
|
158
|
+
|
|
159
|
+
# 添加 demos 目录
|
|
160
|
+
demos_tree = tree.add("Demos 文件")
|
|
161
|
+
demos_dir = directory / "demos"
|
|
162
|
+
if demos_dir.exists() and demos_dir.is_dir():
|
|
163
|
+
for demos_file in demos_dir.rglob('*'):
|
|
164
|
+
if demos_file.is_file():
|
|
165
|
+
zipf.write(demos_file, demos_file.relative_to(directory))
|
|
166
|
+
demos_tree.add(f"+ {demos_file.relative_to(directory)}")
|
|
167
|
+
|
|
168
|
+
# 根据类型添加不同的文件
|
|
169
|
+
md_tree = tree.add("其他文件")
|
|
170
|
+
if config_type == "kit":
|
|
171
|
+
# kit 类型需要添加的文件
|
|
172
|
+
other_files = ["long_description.md", "input.json"]
|
|
173
|
+
else:
|
|
174
|
+
# agent 类型需要添加的文件
|
|
175
|
+
other_files = ["initial_assistant_message.md", "initial_system_prompt.md", "long_description.md"]
|
|
176
|
+
|
|
177
|
+
for file in other_files:
|
|
178
|
+
file_path = directory / "config" / file
|
|
179
|
+
if file_path.exists():
|
|
180
|
+
zipf.write(file_path, f"config/{file}")
|
|
181
|
+
md_tree.add(f"+ config/{file}")
|
|
182
|
+
|
|
183
|
+
tree.add("✅ 压缩包创建完成")
|
|
184
|
+
console.print(tree)
|
|
185
|
+
return zip_name
|
|
186
|
+
|
|
187
|
+
def build_package(directory: Path) -> Tuple[bool, List[str], str]:
|
|
188
|
+
"""构建项目包
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Tuple[bool, List[str], str]: (是否成功, 错误信息列表, zip包名称)
|
|
192
|
+
"""
|
|
193
|
+
console.print(Panel.fit(
|
|
194
|
+
"[bold blue]🚀 开始构建项目包[/bold blue]",
|
|
195
|
+
border_style="blue"
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
all_passed = True
|
|
199
|
+
all_errors = []
|
|
200
|
+
|
|
201
|
+
# 1. 检查 Python 文件
|
|
202
|
+
py_passed, py_errors = check_python_files(directory)
|
|
203
|
+
if not py_passed:
|
|
204
|
+
all_passed = False
|
|
205
|
+
all_errors.extend(py_errors)
|
|
206
|
+
|
|
207
|
+
# 2. 检查配置文件
|
|
208
|
+
config_passed, config_errors = check_configuration(directory)
|
|
209
|
+
if not config_passed:
|
|
210
|
+
all_passed = False
|
|
211
|
+
all_errors.extend(config_errors)
|
|
212
|
+
|
|
213
|
+
# 3. 检查 Markdown 文件
|
|
214
|
+
md_passed, md_errors = check_markdown_files(directory)
|
|
215
|
+
if not md_passed:
|
|
216
|
+
all_passed = False
|
|
217
|
+
all_errors.extend(md_errors)
|
|
218
|
+
|
|
219
|
+
# 如果所有检查都通过,创建 zip 包
|
|
220
|
+
zip_name = ""
|
|
221
|
+
if all_passed:
|
|
222
|
+
zip_name = create_zip_package(directory)
|
|
223
|
+
|
|
224
|
+
if all_passed:
|
|
225
|
+
console.print(Panel.fit(
|
|
226
|
+
f"[bold green]✅ 构建成功![/bold green]\n"
|
|
227
|
+
f"压缩包: {zip_name}",
|
|
228
|
+
border_style="green"
|
|
229
|
+
))
|
|
230
|
+
else:
|
|
231
|
+
console.print(Panel.fit(
|
|
232
|
+
"[bold red]❌ 构建失败![/bold red]\n" + "\n".join(all_errors),
|
|
233
|
+
border_style="red"
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
return all_passed, all_errors, zip_name
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .parser import parse_directory
|
|
5
|
+
from .build import build_package
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def cli():
|
|
9
|
+
"""Adam Community CLI 工具"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@cli.command()
|
|
13
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True), default='.')
|
|
14
|
+
def parse(directory):
|
|
15
|
+
"""解析指定目录下的所有 Python 文件并生成 functions.json"""
|
|
16
|
+
directory_path = Path(directory)
|
|
17
|
+
all_functions = parse_directory(directory_path)
|
|
18
|
+
|
|
19
|
+
# 将结果写入 functions.json
|
|
20
|
+
output_file = directory_path / 'functions.json'
|
|
21
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
22
|
+
json.dump(all_functions, f, indent=2, ensure_ascii=False)
|
|
23
|
+
|
|
24
|
+
click.echo(f"已成功解析 {len(all_functions)} 个类,结果保存在 {output_file}")
|
|
25
|
+
|
|
26
|
+
@cli.command()
|
|
27
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True), default='.')
|
|
28
|
+
def build(directory):
|
|
29
|
+
"""构建项目包"""
|
|
30
|
+
directory_path = Path(directory)
|
|
31
|
+
|
|
32
|
+
# 执行构建
|
|
33
|
+
success, errors, zip_name = build_package(directory_path)
|
|
34
|
+
|
|
35
|
+
if success:
|
|
36
|
+
click.echo(f"包创建成功: {zip_name}")
|
|
37
|
+
else:
|
|
38
|
+
click.echo("检查未通过,发现以下问题:")
|
|
39
|
+
for error in errors:
|
|
40
|
+
click.echo(f"- {error}")
|
|
41
|
+
raise click.Abort()
|
|
42
|
+
|
|
43
|
+
if __name__ == '__main__':
|
|
44
|
+
cli()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from docstring_parser import parse as dsp
|
|
5
|
+
|
|
6
|
+
def parse_python_file(file_path: Path) -> List[Dict[str, Any]]:
|
|
7
|
+
"""解析单个 Python 文件,提取类信息
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
file_path: Python 文件路径
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
List[Dict[str, Any]]: 类信息列表
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
17
|
+
content = f.read()
|
|
18
|
+
|
|
19
|
+
tree = ast.parse(content)
|
|
20
|
+
classes = []
|
|
21
|
+
|
|
22
|
+
for node in ast.walk(tree):
|
|
23
|
+
if isinstance(node, ast.ClassDef):
|
|
24
|
+
# 获取类的文档字符串
|
|
25
|
+
docstring = ast.get_docstring(node) or ""
|
|
26
|
+
|
|
27
|
+
# 获取类的静态变量
|
|
28
|
+
sif_var = None
|
|
29
|
+
network_var = None
|
|
30
|
+
gpu = 0
|
|
31
|
+
cpu = 1
|
|
32
|
+
mem_per_cpu = 4000
|
|
33
|
+
partition = "gpu"
|
|
34
|
+
conda_env = "base"
|
|
35
|
+
|
|
36
|
+
for item in node.body:
|
|
37
|
+
if isinstance(item, ast.Assign):
|
|
38
|
+
for target in item.targets:
|
|
39
|
+
if isinstance(target, ast.Name):
|
|
40
|
+
if target.id == 'SIF':
|
|
41
|
+
if isinstance(item.value, ast.Constant):
|
|
42
|
+
sif_var = item.value.value
|
|
43
|
+
elif target.id == 'NETWORK':
|
|
44
|
+
if isinstance(item.value, ast.Constant):
|
|
45
|
+
network_var = item.value.value
|
|
46
|
+
elif target.id == 'GPU':
|
|
47
|
+
if isinstance(item.value, ast.Constant):
|
|
48
|
+
gpu = item.value.value
|
|
49
|
+
elif target.id == 'CPU':
|
|
50
|
+
if isinstance(item.value, ast.Constant):
|
|
51
|
+
cpu = item.value.value
|
|
52
|
+
elif target.id == 'MEM_PER_CPU':
|
|
53
|
+
if isinstance(item.value, ast.Constant):
|
|
54
|
+
mem_per_cpu = item.value.value
|
|
55
|
+
elif target.id == 'PARTITION':
|
|
56
|
+
if isinstance(item.value, ast.Constant):
|
|
57
|
+
partition = item.value.value
|
|
58
|
+
elif target.id == 'CONDA_ENV':
|
|
59
|
+
if isinstance(item.value, ast.Constant):
|
|
60
|
+
conda_env = item.value.value
|
|
61
|
+
elif target.id == 'DISPLAY_NAME':
|
|
62
|
+
if isinstance(item.value, ast.Constant):
|
|
63
|
+
display_name = item.value.value
|
|
64
|
+
|
|
65
|
+
# 获取类的参数信息
|
|
66
|
+
parameters = {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {},
|
|
69
|
+
"required": []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 使用 docstring_parser 解析类的文档字符串
|
|
73
|
+
docs = dsp(docstring)
|
|
74
|
+
if docs:
|
|
75
|
+
for p in docs.params:
|
|
76
|
+
parameters["properties"][p.arg_name] = {
|
|
77
|
+
"description": p.description or "",
|
|
78
|
+
"type": p.type_name or "string"
|
|
79
|
+
}
|
|
80
|
+
if not p.is_optional:
|
|
81
|
+
parameters["required"].append(p.arg_name)
|
|
82
|
+
|
|
83
|
+
class_info = {
|
|
84
|
+
"function": {
|
|
85
|
+
"name": f"{file_path.stem}.{node.name}",
|
|
86
|
+
"description": docs.short_description if docs else "",
|
|
87
|
+
"parameters": parameters,
|
|
88
|
+
"file": str(file_path),
|
|
89
|
+
"sif": sif_var,
|
|
90
|
+
"network": network_var,
|
|
91
|
+
"gpu": gpu,
|
|
92
|
+
"cpu": cpu,
|
|
93
|
+
"mem_per_cpu": mem_per_cpu,
|
|
94
|
+
"partition": partition,
|
|
95
|
+
"conda_env": conda_env,
|
|
96
|
+
"display_name": display_name
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
classes.append(class_info)
|
|
100
|
+
|
|
101
|
+
return classes
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"解析文件 {file_path} 时出错: {str(e)}")
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
def parse_directory(directory: Path) -> List[Dict[str, Any]]:
|
|
107
|
+
"""解析目录下的所有 Python 文件
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
directory: 目录路径
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List[Dict[str, Any]]: 所有类信息列表
|
|
114
|
+
"""
|
|
115
|
+
all_classes = []
|
|
116
|
+
# 排除 config 目录
|
|
117
|
+
for py_file in directory.rglob('*.py'):
|
|
118
|
+
if not py_file.name.startswith('_') and 'config' not in py_file.parts:
|
|
119
|
+
classes = parse_python_file(py_file)
|
|
120
|
+
all_classes.extend(classes)
|
|
121
|
+
return all_classes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from .util import markdown_color, markdown_terminal
|
|
4
|
+
|
|
5
|
+
class Tool:
|
|
6
|
+
logger = logging.getLogger("ADAM-TOOL")
|
|
7
|
+
|
|
8
|
+
def __init_subclass__(cls, **kwargs):
|
|
9
|
+
super().__init_subclass__()
|
|
10
|
+
|
|
11
|
+
for key, value in kwargs.items():
|
|
12
|
+
setattr(cls, key, value)
|
|
13
|
+
if not hasattr(cls, "GPU"):
|
|
14
|
+
cls.GPU = 0
|
|
15
|
+
if not hasattr(cls, "CPU"):
|
|
16
|
+
cls.CPU = 1
|
|
17
|
+
if not hasattr(cls, "MEM_PER_CPU"):
|
|
18
|
+
cls.MEM_PER_CPU = 4000
|
|
19
|
+
if not hasattr(cls, "PARTITION"):
|
|
20
|
+
cls.PARTITION = "gpu_3090"
|
|
21
|
+
if not hasattr(cls, "CONDA_ENV"):
|
|
22
|
+
cls.CONDA_ENV = "base"
|
|
23
|
+
if not hasattr(cls, "calltype"):
|
|
24
|
+
cls.calltype = "bash"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def inputShow(self, **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
AI决定使用该工具时调用,用于打印信息告知用户目前调用的工具
|
|
30
|
+
此处的kwargs更改对后续过程无效
|
|
31
|
+
|
|
32
|
+
:param dict args: 字典形式的AI产生的输入
|
|
33
|
+
:param list[str] files: 此次用户上传的文件
|
|
34
|
+
:param str message: 此次用户询问的消息
|
|
35
|
+
:param str user: 用户昵称
|
|
36
|
+
:param str task_id: 任务编号
|
|
37
|
+
"""
|
|
38
|
+
if self.calltype == "bash":
|
|
39
|
+
os.environ["ADAM_OUTPUT_RAW"] = "true"
|
|
40
|
+
scripts = self.call(self, kwargs)
|
|
41
|
+
os.environ["ADAM_OUTPUT_RAW"] = "false"
|
|
42
|
+
|
|
43
|
+
scripts = '\n'.join(['bash -s << EOF', scripts, 'EOF'])
|
|
44
|
+
else:
|
|
45
|
+
scripts = f"""python {self.__name__}.py"""
|
|
46
|
+
return Tool.markdown_terminal(scripts, workdir=kwargs["task_id"],
|
|
47
|
+
user=kwargs["user"], conda_env=self.CONDA_ENV)
|
|
48
|
+
|
|
49
|
+
def resAlloc(self, kwargs):
|
|
50
|
+
"""
|
|
51
|
+
提交工具任务时调用,用于获取分配的资源量
|
|
52
|
+
此处的kwargs更改只有tool_data和tip对后续过程有效
|
|
53
|
+
|
|
54
|
+
kwarg在inputShow的基础之上新增:
|
|
55
|
+
:param any tool_data: 可序列化的任意数据
|
|
56
|
+
:param str tip: 用来给用户显示的提示
|
|
57
|
+
:returns: 一个字典,包括使用的GPU、CPU和MEM_PER_CPU(以MB单位的整数)
|
|
58
|
+
"""
|
|
59
|
+
return {"CPU": self.CPU, "MEM_PER_CPU": self.MEM_PER_CPU,
|
|
60
|
+
"GPU": self.GPU, "PARTITION": self.PARTITION}
|
|
61
|
+
|
|
62
|
+
def call(self, kwargs):
|
|
63
|
+
"""
|
|
64
|
+
实际执行脚本
|
|
65
|
+
此处的kwargs更改只有tool_data,tip和token_usage对后续过程有效
|
|
66
|
+
|
|
67
|
+
kwarg同resAlloc
|
|
68
|
+
:returns: calltype=bash时,返回实际运行的bash脚本;
|
|
69
|
+
calltype=python时,return调用值无效
|
|
70
|
+
"""
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def monitor(self, kwargs):
|
|
75
|
+
"""
|
|
76
|
+
任务运行时的监控命令,用于告知用户目前的进度
|
|
77
|
+
只有在return为True时,此处的kwargs更改tool_data、tip和progress对后续过程有效
|
|
78
|
+
|
|
79
|
+
kwargs在call的基础之上新增:
|
|
80
|
+
:param int run_time: 以秒为单位的运行时间
|
|
81
|
+
:param float progress: 任务进度,1为任务完成,0.5为50%,以此类推
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def outputShow(self, kwargs):
|
|
86
|
+
"""
|
|
87
|
+
任务结束时调用,用于打印信息告知用户工具结果
|
|
88
|
+
|
|
89
|
+
kwargs在monitor的基础上新增:
|
|
90
|
+
:param str stdout: stdout流的结果
|
|
91
|
+
:param str stderr: stderr流的结果
|
|
92
|
+
:param int exit_code: 退出码
|
|
93
|
+
:returns: 一个给用户的文本, 一个包含显示文件的列表
|
|
94
|
+
"""
|
|
95
|
+
stdout = kwargs["stdout"]
|
|
96
|
+
if len(stdout) > 10000:
|
|
97
|
+
stdout = stdout[:5000] + "\n...输出太长已省略...\n" + stdout[-5000:]
|
|
98
|
+
|
|
99
|
+
stderr = kwargs["stderr"]
|
|
100
|
+
if len(stderr) > 10000:
|
|
101
|
+
stderr = stderr[:5000] + "\n...输出太长已省略...\n" + stderr[-5000:]
|
|
102
|
+
return f'''- exit code: {kwargs['exit_code']}
|
|
103
|
+
- stdout:
|
|
104
|
+
```
|
|
105
|
+
{stdout}
|
|
106
|
+
```
|
|
107
|
+
- stderr:
|
|
108
|
+
```
|
|
109
|
+
{stderr}
|
|
110
|
+
```
|
|
111
|
+
''', []
|
|
112
|
+
|
|
113
|
+
def summary(self, kwargs):
|
|
114
|
+
"""
|
|
115
|
+
任务结束时调用,用于打印信息告知AI结果
|
|
116
|
+
|
|
117
|
+
kwargs同outputShow
|
|
118
|
+
:returns: 一个给AI的文本
|
|
119
|
+
"""
|
|
120
|
+
stdout = kwargs["stdout"]
|
|
121
|
+
if len(stdout) > 10000:
|
|
122
|
+
stdout = stdout[:5000] + "\n...输出太长已省略...\n" + stdout[-5000:]
|
|
123
|
+
stderr = kwargs["stderr"]
|
|
124
|
+
if len(stderr) > 10000:
|
|
125
|
+
stderr = stderr[:5000] + "\n...输出太长已省略...\n" + stderr[-5000:]
|
|
126
|
+
return f'''- exit code: {kwargs['exit_code']}
|
|
127
|
+
- stdout:
|
|
128
|
+
```
|
|
129
|
+
{stdout}
|
|
130
|
+
```
|
|
131
|
+
- stderr:
|
|
132
|
+
```
|
|
133
|
+
{stderr}
|
|
134
|
+
```
|
|
135
|
+
'''
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def markdown_color(content, color):
|
|
139
|
+
return markdown_color(content, color)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def markdown_terminal(content, conda_env="base", user="Adam", workdir=""):
|
|
143
|
+
return markdown_terminal(content, conda_env, user, workdir)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import urllib.request
|
|
3
|
+
import urllib.error
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
import select
|
|
9
|
+
import logging
|
|
10
|
+
import ssl
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from subprocess import run, CalledProcessError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
ADAM_API_HOST = os.getenv('ADAM_API_HOST', 'https://sidereus-ai.com')
|
|
17
|
+
ADAM_API_TOKEN = os.getenv('ADAM_API_TOKEN')
|
|
18
|
+
ADAM_TASK_ID = os.getenv('ADAM_TASK_ID')
|
|
19
|
+
ADAM_USER_ID = os.getenv('ADAM_USER_ID')
|
|
20
|
+
CONDA_ENV = os.getenv('CONDA_ENV')
|
|
21
|
+
|
|
22
|
+
# 定义可用的知识库集合
|
|
23
|
+
collections = ["DSDP","MPNN","PySCF","RFdiffusion","gaussian","protenix","GPU4PySCF","MolecularDynamics","RDkit","OpenBabel", "SPONGE", "Xponge", "MDAnalysis", "ABACUS"]
|
|
24
|
+
|
|
25
|
+
class RAG:
|
|
26
|
+
"""
|
|
27
|
+
搜寻RAG知识
|
|
28
|
+
|
|
29
|
+
:param str query: 需要运行的命令
|
|
30
|
+
:param str collection: 搜寻的知识库名称。必须是下面之一:"DSDP","MPNN","PySCF","RFdiffusion","gaussian","protenix","GPU4PySCF","MolecularDynamics","RDkit","OpenBabel"
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def call(self, query: str, collection: str):
|
|
34
|
+
|
|
35
|
+
if collection not in collections:
|
|
36
|
+
raise ValueError(f"知识库名称必须是下面之一:{collections}")
|
|
37
|
+
|
|
38
|
+
cmd = f"""akb simple-query \
|
|
39
|
+
-i /share/programs/akb/database/{collection}/{collection}.index \
|
|
40
|
+
-m /share/programs/BAAI/bge-m3 \
|
|
41
|
+
-p /share/programs/akb/database/{collection}/{collection}.parquet \
|
|
42
|
+
-l 5 \
|
|
43
|
+
-f json -q \"{query}\""""
|
|
44
|
+
logger.info(f"搜索知识库 {collection}: {query}")
|
|
45
|
+
try:
|
|
46
|
+
result = run(cmd, shell='/bin/bash', check=True, text=True, capture_output=True)
|
|
47
|
+
except CalledProcessError as e:
|
|
48
|
+
logger.error(e.stderr.strip())
|
|
49
|
+
return e.stderr.strip()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
return "\n".join(json.loads(result.stdout))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(e)
|
|
55
|
+
return result.stdout
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def retry_on_exception(max_retries=3, delay=5):
|
|
59
|
+
"""
|
|
60
|
+
重试装饰器,在发生异常时最多重试指定次数
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
max_retries: 最大重试次数
|
|
64
|
+
delay: 重试间隔时间(秒)
|
|
65
|
+
"""
|
|
66
|
+
def decorator(func):
|
|
67
|
+
@wraps(func)
|
|
68
|
+
def wrapper(*args, **kwargs):
|
|
69
|
+
last_exception = None
|
|
70
|
+
for attempt in range(max_retries):
|
|
71
|
+
try:
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
last_exception = e
|
|
75
|
+
if attempt < max_retries - 1:
|
|
76
|
+
time.sleep(delay)
|
|
77
|
+
continue
|
|
78
|
+
raise last_exception
|
|
79
|
+
return None
|
|
80
|
+
return wrapper
|
|
81
|
+
return decorator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _make_http_request(url, data, return_json=True):
|
|
85
|
+
"""
|
|
86
|
+
通用的 HTTP POST 请求函数
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
url: 请求的 URL
|
|
90
|
+
data: 请求数据(字典格式)
|
|
91
|
+
return_json: 是否将响应解析为 JSON,False 则返回文本
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
响应数据(JSON 或文本)
|
|
95
|
+
"""
|
|
96
|
+
if not ADAM_API_TOKEN:
|
|
97
|
+
raise ValueError("ADAM_API_TOKEN environment variable is not set")
|
|
98
|
+
|
|
99
|
+
headers = {
|
|
100
|
+
"Authorization": f"Bearer {ADAM_API_TOKEN}",
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# 将请求数据转换为JSON字符串并编码
|
|
106
|
+
json_data = json.dumps(data).encode('utf-8')
|
|
107
|
+
|
|
108
|
+
# 创建请求对象
|
|
109
|
+
req = urllib.request.Request(url, data=json_data, headers=headers, method='POST')
|
|
110
|
+
|
|
111
|
+
context = ssl._create_unverified_context()
|
|
112
|
+
|
|
113
|
+
# 发送请求
|
|
114
|
+
with urllib.request.urlopen(req, context=context) as response:
|
|
115
|
+
# 检查响应状态码
|
|
116
|
+
if response.status >= 400:
|
|
117
|
+
raise Exception(f"HTTP错误: {response.status}")
|
|
118
|
+
|
|
119
|
+
# 读取响应
|
|
120
|
+
response_data = response.read().decode('utf-8')
|
|
121
|
+
|
|
122
|
+
# 根据需要返回 JSON 或文本
|
|
123
|
+
if return_json:
|
|
124
|
+
return json.loads(response_data)
|
|
125
|
+
else:
|
|
126
|
+
return response_data
|
|
127
|
+
|
|
128
|
+
except urllib.error.HTTPError as e:
|
|
129
|
+
raise Exception(f"HTTP请求失败 - HTTP错误 {e.code}: {e.reason}")
|
|
130
|
+
except urllib.error.URLError as e:
|
|
131
|
+
raise Exception(f"HTTP请求失败 - 网络错误: {str(e)}")
|
|
132
|
+
except json.JSONDecodeError as e:
|
|
133
|
+
if return_json:
|
|
134
|
+
raise Exception(f"HTTP请求失败 - JSON解析错误: {str(e)}")
|
|
135
|
+
else:
|
|
136
|
+
raise Exception(f"HTTP请求失败 - 响应解码错误: {str(e)}")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise Exception(f"HTTP请求失败: {str(e)}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def messageSend(message):
|
|
142
|
+
"""
|
|
143
|
+
发送消息给用户
|
|
144
|
+
"""
|
|
145
|
+
if not ADAM_API_HOST:
|
|
146
|
+
raise ValueError("ADAM_API_HOST environment variable is not set")
|
|
147
|
+
if not ADAM_TASK_ID:
|
|
148
|
+
raise ValueError("ADAM_TASK_ID environment variable is not set")
|
|
149
|
+
|
|
150
|
+
url = f"{ADAM_API_HOST}/api/task/create_message"
|
|
151
|
+
|
|
152
|
+
request_data = {
|
|
153
|
+
"task_id": ADAM_TASK_ID,
|
|
154
|
+
"message": message,
|
|
155
|
+
"role": "tool"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
return _make_http_request(url, request_data, return_json=True)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise Exception(f"发送消息失败: {str(e)}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@retry_on_exception(max_retries=3)
|
|
165
|
+
def knowledgeSearch(query_info, messages_prev, project_name, collection_name, max_messages=4):
|
|
166
|
+
"""
|
|
167
|
+
知识库检索函数
|
|
168
|
+
"""
|
|
169
|
+
if not collection_name:
|
|
170
|
+
raise ValueError("collection_name 参数是必需的")
|
|
171
|
+
|
|
172
|
+
if collection_name not in collections:
|
|
173
|
+
raise ValueError(f"知识库名称必须是下面之一:{collections}")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# 使用本地RAG实现
|
|
177
|
+
rag = RAG()
|
|
178
|
+
result = rag.call(query_info, collection_name)
|
|
179
|
+
|
|
180
|
+
# 构造返回格式,保持与原有API格式一致
|
|
181
|
+
response_data = {
|
|
182
|
+
"code": 0,
|
|
183
|
+
"data": {
|
|
184
|
+
"collection_name": collection_name,
|
|
185
|
+
"count": 1,
|
|
186
|
+
"result_list": [{
|
|
187
|
+
"chunk_id": 0,
|
|
188
|
+
"chunk_source": "document",
|
|
189
|
+
"chunk_title": result.split("\n")[0] if result else "",
|
|
190
|
+
"chunk_type": "text",
|
|
191
|
+
"content": result,
|
|
192
|
+
"doc_info": {
|
|
193
|
+
"create_time": 0,
|
|
194
|
+
"doc_id": "local_doc",
|
|
195
|
+
"doc_name": f"{collection_name}_knowledge",
|
|
196
|
+
"doc_type": "text",
|
|
197
|
+
"source": "local"
|
|
198
|
+
},
|
|
199
|
+
"score": 1.0
|
|
200
|
+
}],
|
|
201
|
+
"token_usage": {
|
|
202
|
+
"embedding_token_usage": {
|
|
203
|
+
"completion_tokens": 0,
|
|
204
|
+
"prompt_tokens": 0,
|
|
205
|
+
"total_tokens": 0
|
|
206
|
+
},
|
|
207
|
+
"rerank_token_usage": 0
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"message": "success",
|
|
211
|
+
"request_id": "local_request"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return json.dumps(response_data, ensure_ascii=False)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
error_response = {
|
|
217
|
+
"code": 1,
|
|
218
|
+
"message": f"知识库搜索失败: {str(e)}"
|
|
219
|
+
}
|
|
220
|
+
return json.dumps(error_response, ensure_ascii=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class DynamicObject:
|
|
224
|
+
"""
|
|
225
|
+
动态对象类,用于处理 JSON 响应
|
|
226
|
+
|
|
227
|
+
这个类可以动态地将字典转换为对象属性,支持嵌套的字典结构
|
|
228
|
+
同时保持原始数据的访问能力
|
|
229
|
+
"""
|
|
230
|
+
def __init__(self, data):
|
|
231
|
+
self._raw_data = data
|
|
232
|
+
if isinstance(data, dict):
|
|
233
|
+
for key, value in data.items():
|
|
234
|
+
if isinstance(value, (dict, list)):
|
|
235
|
+
setattr(self, key, self._convert_value(value))
|
|
236
|
+
else:
|
|
237
|
+
setattr(self, key, value)
|
|
238
|
+
|
|
239
|
+
def _convert_value(self, value):
|
|
240
|
+
"""递归转换嵌套的数据结构"""
|
|
241
|
+
if isinstance(value, dict):
|
|
242
|
+
return DynamicObject(value)
|
|
243
|
+
elif isinstance(value, list):
|
|
244
|
+
return [self._convert_value(item) for item in value]
|
|
245
|
+
return value
|
|
246
|
+
|
|
247
|
+
def __getattr__(self, name):
|
|
248
|
+
"""处理未定义的属性访问"""
|
|
249
|
+
return self._raw_data.get(name)
|
|
250
|
+
|
|
251
|
+
def __getitem__(self, key):
|
|
252
|
+
"""支持字典式访问"""
|
|
253
|
+
return self._raw_data.get(key)
|
|
254
|
+
|
|
255
|
+
def __str__(self):
|
|
256
|
+
"""返回可读的字符串表示"""
|
|
257
|
+
return f"DynamicObject({self._raw_data})"
|
|
258
|
+
|
|
259
|
+
def __repr__(self):
|
|
260
|
+
return self.__str__()
|
|
261
|
+
|
|
262
|
+
def to_dict(self):
|
|
263
|
+
"""将对象转换回字典"""
|
|
264
|
+
return self._raw_data
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@retry_on_exception(max_retries=3)
|
|
268
|
+
def completionCreate(request_params):
|
|
269
|
+
"""
|
|
270
|
+
创建 openai 的代理函数
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
DynamicObject: 一个动态对象,可以像访问属性一样访问 API 响应的所有字段
|
|
274
|
+
"""
|
|
275
|
+
if not ADAM_API_HOST:
|
|
276
|
+
raise ValueError("ADAM_API_HOST environment variable is not set")
|
|
277
|
+
|
|
278
|
+
url = f"{ADAM_API_HOST}/api/chat/completions"
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
response = _make_http_request(url, request_params, return_json=True)
|
|
282
|
+
return DynamicObject(response)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
raise Exception(f"调用聊天补全接口失败: {str(e)}")
|
|
285
|
+
|
|
286
|
+
def runCmd(cmd):
|
|
287
|
+
"""
|
|
288
|
+
执行命令,实时输出执行结果
|
|
289
|
+
|
|
290
|
+
修复了原实现中可能存在的阻塞和内容丢失问题
|
|
291
|
+
"""
|
|
292
|
+
if os.getenv("ADAM_OUTPUT_RAW"):
|
|
293
|
+
return cmd
|
|
294
|
+
|
|
295
|
+
process = subprocess.Popen(
|
|
296
|
+
cmd,
|
|
297
|
+
shell=True,
|
|
298
|
+
executable='/bin/bash',
|
|
299
|
+
stdout=subprocess.PIPE,
|
|
300
|
+
stderr=subprocess.PIPE,
|
|
301
|
+
universal_newlines=True,
|
|
302
|
+
bufsize=1
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
# 使用 select 来同时监控 stdout 和 stderr,避免阻塞
|
|
307
|
+
while True:
|
|
308
|
+
# 检查进程是否已结束
|
|
309
|
+
if process.poll() is not None:
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
# 使用 select 来检查是否有可读的数据
|
|
313
|
+
rlist, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
|
314
|
+
|
|
315
|
+
for stream in rlist:
|
|
316
|
+
line = stream.readline()
|
|
317
|
+
if line:
|
|
318
|
+
if stream == process.stdout:
|
|
319
|
+
print(line.strip())
|
|
320
|
+
else:
|
|
321
|
+
print(line.strip(), file=sys.stderr)
|
|
322
|
+
|
|
323
|
+
# 读取剩余的输出(如果有)
|
|
324
|
+
for line in process.stdout:
|
|
325
|
+
print(line.strip())
|
|
326
|
+
for line in process.stderr:
|
|
327
|
+
print(line.strip(), file=sys.stderr)
|
|
328
|
+
|
|
329
|
+
# 检查返回码
|
|
330
|
+
if process.returncode != 0:
|
|
331
|
+
sys.exit(process.returncode)
|
|
332
|
+
|
|
333
|
+
return process
|
|
334
|
+
finally:
|
|
335
|
+
# 确保所有文件描述符都被关闭
|
|
336
|
+
process.stdout.close()
|
|
337
|
+
process.stderr.close()
|
|
338
|
+
|
|
339
|
+
def markdown_color(content, color):
|
|
340
|
+
return f'<span style="color: {color}">{content}</span>'
|
|
341
|
+
|
|
342
|
+
def markdown_terminal(content, conda_env="base", user="Adam", workdir=""):
|
|
343
|
+
user = markdown_color(f"{user}@Adam", "green")
|
|
344
|
+
workdir = markdown_color(f":~/{workdir}", "blue")
|
|
345
|
+
return f'({conda_env}) {user}{workdir}$ {content}'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adam_community
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: Adam Community Tools and Utilities
|
|
5
|
+
Home-page: https://github.com/yourusername/adam-community
|
|
6
|
+
Author: Adam Community
|
|
7
|
+
Author-email: admin@sidereus-ai.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: click>=8.0.0
|
|
15
|
+
Requires-Dist: docstring-parser>=0.15
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# Adam Community
|
|
28
|
+
|
|
29
|
+
Adam Community 是一个 Python 工具包,提供了一系列实用工具和功能,用于与 Adam 社区相关的开发工作。
|
|
30
|
+
|
|
31
|
+
安装依赖:
|
|
32
|
+
```bash
|
|
33
|
+
make install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## 使用方法
|
|
38
|
+
|
|
39
|
+
安装完成后,您可以在 Python 代码中导入并使用该包:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from adam_community import tool
|
|
43
|
+
from adam_community import util
|
|
44
|
+
|
|
45
|
+
# 使用相关功能
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 开发
|
|
49
|
+
|
|
50
|
+
运行测试:
|
|
51
|
+
```bash
|
|
52
|
+
make test
|
|
53
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
adam_community/__init__.py
|
|
4
|
+
adam_community/tool.py
|
|
5
|
+
adam_community/util.py
|
|
6
|
+
adam_community.egg-info/PKG-INFO
|
|
7
|
+
adam_community.egg-info/SOURCES.txt
|
|
8
|
+
adam_community.egg-info/dependency_links.txt
|
|
9
|
+
adam_community.egg-info/entry_points.txt
|
|
10
|
+
adam_community.egg-info/requires.txt
|
|
11
|
+
adam_community.egg-info/top_level.txt
|
|
12
|
+
adam_community/cli/__init__.py
|
|
13
|
+
adam_community/cli/build.py
|
|
14
|
+
adam_community/cli/cli.py
|
|
15
|
+
adam_community/cli/parser.py
|
|
16
|
+
test/test_util_tool.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adam_community
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="adam_community",
|
|
6
|
+
version="0.2",
|
|
7
|
+
packages=find_packages(),
|
|
8
|
+
install_requires=[
|
|
9
|
+
"requests>=2.31.0",
|
|
10
|
+
"click>=8.0.0",
|
|
11
|
+
"docstring-parser>=0.15",
|
|
12
|
+
"rich>=13.0.0",
|
|
13
|
+
],
|
|
14
|
+
entry_points={
|
|
15
|
+
'console_scripts': [
|
|
16
|
+
'adam-cli=adam_community.cli.cli:cli',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
author="Adam Community",
|
|
20
|
+
author_email="admin@sidereus-ai.com",
|
|
21
|
+
description="Adam Community Tools and Utilities",
|
|
22
|
+
long_description=open("README.md").read() if os.path.exists("README.md") else "",
|
|
23
|
+
long_description_content_type="text/markdown",
|
|
24
|
+
url="https://github.com/yourusername/adam-community",
|
|
25
|
+
classifiers=[
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
],
|
|
30
|
+
python_requires=">=3.8",
|
|
31
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock, PropertyMock
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# 设置环境变量
|
|
7
|
+
os.environ['ADAM_API_TOKEN'] = 'test_token'
|
|
8
|
+
os.environ['ADAM_API_HOST'] = 'https://test.com'
|
|
9
|
+
|
|
10
|
+
# 添加项目根目录到 Python 路径
|
|
11
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
|
+
|
|
13
|
+
# Mock OpenAI client before importing util
|
|
14
|
+
with patch('openai.OpenAI'):
|
|
15
|
+
from adam_community.util import knowledgeSearch, completionCreate, runCmd
|
|
16
|
+
from adam_community.tool import Tool
|
|
17
|
+
|
|
18
|
+
class TestUtil(unittest.TestCase):
|
|
19
|
+
def setUp(self):
|
|
20
|
+
self.test_params = {
|
|
21
|
+
"project": "test_project",
|
|
22
|
+
"name": "test_collection",
|
|
23
|
+
"query": "test query",
|
|
24
|
+
"limit": 10
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@patch('urllib.request.urlopen')
|
|
28
|
+
def test_knowledgeSearch(self, mock_urlopen):
|
|
29
|
+
"""测试knowledgeSearch函数"""
|
|
30
|
+
mock_response = MagicMock()
|
|
31
|
+
# 设置status属性为整数
|
|
32
|
+
type(mock_response).status = PropertyMock(return_value=200)
|
|
33
|
+
mock_response.read.return_value = b'{"result": "success"}'
|
|
34
|
+
mock_urlopen.return_value.__enter__.return_value = mock_response
|
|
35
|
+
|
|
36
|
+
result = knowledgeSearch(
|
|
37
|
+
query_info="test query",
|
|
38
|
+
messages_prev=[{"role": "user", "content": "test"}],
|
|
39
|
+
project_name="test_project",
|
|
40
|
+
collection_name="test_collection"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.assertEqual(result, '{"result": "success"}')
|
|
44
|
+
mock_urlopen.assert_called_once()
|
|
45
|
+
|
|
46
|
+
@patch('subprocess.Popen')
|
|
47
|
+
@patch.dict(os.environ, {'ADAM_OUTPUT_RAW': ''}, clear=True)
|
|
48
|
+
def test_runCmd_success(self, mock_popen):
|
|
49
|
+
"""测试runCmd函数成功执行命令的情况"""
|
|
50
|
+
mock_process = MagicMock()
|
|
51
|
+
mock_process.poll.return_value = 0
|
|
52
|
+
mock_process.returncode = 0
|
|
53
|
+
mock_process.stdout.readline.return_value = "test output"
|
|
54
|
+
mock_process.stderr.readline.return_value = ""
|
|
55
|
+
mock_popen.return_value = mock_process
|
|
56
|
+
|
|
57
|
+
process = runCmd('echo "test output"')
|
|
58
|
+
self.assertEqual(process.returncode, 0)
|
|
59
|
+
mock_popen.assert_called_once()
|
|
60
|
+
|
|
61
|
+
@patch('subprocess.Popen')
|
|
62
|
+
@patch.dict(os.environ, {'ADAM_OUTPUT_RAW': ''}, clear=True)
|
|
63
|
+
def test_runCmd_failure(self, mock_popen):
|
|
64
|
+
"""测试runCmd函数执行失败的情况"""
|
|
65
|
+
mock_process = MagicMock()
|
|
66
|
+
mock_process.poll.return_value = 1
|
|
67
|
+
mock_process.returncode = 1
|
|
68
|
+
mock_process.stdout.readline.return_value = ""
|
|
69
|
+
mock_process.stderr.readline.return_value = "command not found"
|
|
70
|
+
mock_popen.return_value = mock_process
|
|
71
|
+
|
|
72
|
+
with self.assertRaises(SystemExit) as cm:
|
|
73
|
+
runCmd('nonexistent_command_123456')
|
|
74
|
+
self.assertEqual(cm.exception.code, 1)
|
|
75
|
+
|
|
76
|
+
@patch.dict(os.environ, {'ADAM_OUTPUT_RAW': 'true'}, clear=True)
|
|
77
|
+
def test_runCmd_with_raw_output(self):
|
|
78
|
+
"""测试runCmd函数在ADAM_OUTPUT_RAW环境变量设置时的情况"""
|
|
79
|
+
cmd = 'echo "test output"'
|
|
80
|
+
result = runCmd(cmd)
|
|
81
|
+
self.assertEqual(result, cmd)
|
|
82
|
+
|
|
83
|
+
class TestTool(unittest.TestCase):
|
|
84
|
+
def setUp(self):
|
|
85
|
+
class TestToolImpl(Tool):
|
|
86
|
+
def call(self, kwargs):
|
|
87
|
+
return "test command"
|
|
88
|
+
|
|
89
|
+
self.tool = TestToolImpl()
|
|
90
|
+
|
|
91
|
+
def test_inputShow(self):
|
|
92
|
+
"""测试inputShow方法"""
|
|
93
|
+
kwargs = {
|
|
94
|
+
"task_id": "test_task",
|
|
95
|
+
"user": "test_user",
|
|
96
|
+
"message": "test message"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
with patch.object(self.tool, 'call', return_value="test command"):
|
|
100
|
+
result = self.tool.inputShow(**kwargs)
|
|
101
|
+
self.assertIn("test_user@Adam", result)
|
|
102
|
+
self.assertIn("test_task", result)
|
|
103
|
+
|
|
104
|
+
def test_resAlloc(self):
|
|
105
|
+
"""测试resAlloc方法"""
|
|
106
|
+
kwargs = {
|
|
107
|
+
"tool_data": "test_data",
|
|
108
|
+
"tip": "test tip"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
result = self.tool.resAlloc(kwargs)
|
|
112
|
+
self.assertIn("CPU", result)
|
|
113
|
+
self.assertIn("MEM_PER_CPU", result)
|
|
114
|
+
self.assertIn("GPU", result)
|
|
115
|
+
self.assertIn("PARTITION", result)
|
|
116
|
+
|
|
117
|
+
def test_outputShow(self):
|
|
118
|
+
"""测试outputShow方法"""
|
|
119
|
+
kwargs = {
|
|
120
|
+
"stdout": "test stdout",
|
|
121
|
+
"stderr": "test stderr",
|
|
122
|
+
"exit_code": 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
result, files = self.tool.outputShow(kwargs)
|
|
126
|
+
self.assertIn("test stdout", result)
|
|
127
|
+
self.assertIn("test stderr", result)
|
|
128
|
+
self.assertEqual(files, [])
|
|
129
|
+
|
|
130
|
+
def test_markdown_color(self):
|
|
131
|
+
"""测试markdown_color静态方法"""
|
|
132
|
+
result = Tool.markdown_color("test", "red")
|
|
133
|
+
self.assertEqual(result, '<span style="color: red">test</span>')
|
|
134
|
+
|
|
135
|
+
def test_markdown_terminal(self):
|
|
136
|
+
"""测试markdown_terminal静态方法"""
|
|
137
|
+
result = Tool.markdown_terminal("test command", "test_env", "test_user", "test_dir")
|
|
138
|
+
self.assertIn("test_user@Adam", result)
|
|
139
|
+
self.assertIn("test_dir", result)
|
|
140
|
+
self.assertIn("test command", result)
|
|
141
|
+
|
|
142
|
+
if __name__ == '__main__':
|
|
143
|
+
unittest.main()
|