clovers-agent-toolkit 0.1.0__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.
- clovers_agent_toolkit-0.1.0/PKG-INFO +8 -0
- clovers_agent_toolkit-0.1.0/README.md +0 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/__init__.py +6 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/config.py +20 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/fetch.py +67 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/toolkit.py +3 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/workspace/__init__.py +115 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit/workspace/docker.py +89 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit.egg-info/PKG-INFO +8 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit.egg-info/SOURCES.txt +13 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit.egg-info/dependency_links.txt +1 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit.egg-info/requires.txt +2 -0
- clovers_agent_toolkit-0.1.0/clovers_agent_toolkit.egg-info/top_level.txt +1 -0
- clovers_agent_toolkit-0.1.0/pyproject.toml +11 -0
- clovers_agent_toolkit-0.1.0/setup.cfg +4 -0
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from clovers.config import Config as CloversConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
BRAVE_API_KEY: str
|
|
7
|
+
use_shell: bool = True
|
|
8
|
+
"""是否使用shell"""
|
|
9
|
+
session_workspace: bool = True
|
|
10
|
+
"""为每个会话创建一个工作空间"""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def sync_config(cls):
|
|
14
|
+
"""获取 `CloversConfig.environ()[__package__]` 配置并将默认配置同步到全局配置中。"""
|
|
15
|
+
__config_dict__: dict = CloversConfig.environ().setdefault(__package__, {})
|
|
16
|
+
__config_dict__.update((__config__ := cls.model_validate(__config_dict__)).model_dump())
|
|
17
|
+
return __config__
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__config__ = Config.sync_config()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from clovers_agent import Event, CloversAgent
|
|
2
|
+
from .toolkit import toolkit
|
|
3
|
+
from .config import __config__
|
|
4
|
+
|
|
5
|
+
BRAVE_API_KEY = __config__.BRAVE_API_KEY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@toolkit.tool(
|
|
9
|
+
"web_search",
|
|
10
|
+
"联网搜索",
|
|
11
|
+
{"query": {"type": "string", "description": "搜索关键词"}},
|
|
12
|
+
["从URL获取资源"],
|
|
13
|
+
)
|
|
14
|
+
async def _(agent: CloversAgent, event: Event, query: list[str]):
|
|
15
|
+
headers = {"Accept": "application/json", "Accept-Encoding": "gzip", "X-Subscription-Token": BRAVE_API_KEY}
|
|
16
|
+
url = "https://api.search.brave.com/res/v1/web/search"
|
|
17
|
+
params = {"q": query, "count": 8}
|
|
18
|
+
resp = await agent.async_client.get(url, headers=headers, params=params, timeout=20.0)
|
|
19
|
+
if resp.status_code != 200:
|
|
20
|
+
return f"搜索失败,状态码:{resp.status_code}"
|
|
21
|
+
try:
|
|
22
|
+
results = resp.json()["web"]["results"]
|
|
23
|
+
except KeyError:
|
|
24
|
+
return "服务器错误,请稍后再试。"
|
|
25
|
+
if not results:
|
|
26
|
+
return f"未找到关于 '{query}' 的相关搜索结果。"
|
|
27
|
+
md_output = [f"### 关于 '{query}' 的搜索结果:\n"]
|
|
28
|
+
for idx, item in enumerate(results, 1):
|
|
29
|
+
title = item.get("title", "无标题")
|
|
30
|
+
link = item.get("url", "#")
|
|
31
|
+
snippet = item.get("description", "无摘要")
|
|
32
|
+
md_output.append(f"{idx}. **[{title}]({link})**\n 摘要: {snippet}\n")
|
|
33
|
+
return "\n".join(md_output)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@toolkit.tool(
|
|
37
|
+
"web_extractor",
|
|
38
|
+
"读取指定 URL 的网页纯文本内容。当需要从特定网页获取文本信息时使用。",
|
|
39
|
+
{"webpage_url": {"type": "string", "description": "网页的完整 URL 地址"}},
|
|
40
|
+
["从URL获取资源"],
|
|
41
|
+
)
|
|
42
|
+
async def _(agent: CloversAgent, event: Event, webpage_url: str):
|
|
43
|
+
if not webpage_url.startswith("http"):
|
|
44
|
+
webpage_url = f"https://{webpage_url}"
|
|
45
|
+
try:
|
|
46
|
+
resp = await agent.async_client.get(webpage_url)
|
|
47
|
+
if resp.status_code != 200:
|
|
48
|
+
return f"获取网页失败,状态码:{resp.status_code}"
|
|
49
|
+
return resp.text
|
|
50
|
+
except Exception:
|
|
51
|
+
return "获取网页失败"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@toolkit.tool(
|
|
55
|
+
"view_image_url",
|
|
56
|
+
"查看网络图片。当你需要查看用户提供的图片链接时,请调用此工具",
|
|
57
|
+
{"image_url": {"type": "string", "description": "图片的完整 URL 地址"}},
|
|
58
|
+
["从URL获取资源"],
|
|
59
|
+
)
|
|
60
|
+
async def _(agent: CloversAgent, event: Event, image_url: str):
|
|
61
|
+
if not image_url.startswith("http"):
|
|
62
|
+
image_url = f"https://{image_url}"
|
|
63
|
+
assert agent.current_input
|
|
64
|
+
if isinstance(agent.current_input["content"], str):
|
|
65
|
+
agent.current_input["content"] = [{"type": "text", "text": agent.current_input["content"]}]
|
|
66
|
+
agent.current_input["content"].append({"type": "image_url", "image_url": {"url": image_url}})
|
|
67
|
+
return "图片已放入用户上下文"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from clovers_agent import Event, CloversAgent
|
|
3
|
+
from clovers.logger import logger
|
|
4
|
+
from .docker import WORKSPACE, Shell
|
|
5
|
+
from ..toolkit import toolkit
|
|
6
|
+
from ..config import __config__
|
|
7
|
+
|
|
8
|
+
README = WORKSPACE / "README.md"
|
|
9
|
+
GITIGNORE = WORKSPACE / ".gitignore"
|
|
10
|
+
shell_dict: dict[str, Shell] = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_session_id(agent: CloversAgent, event: Event) -> str: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __config__.session_workspace:
|
|
17
|
+
get_session_id = lambda agent, event: agent.session_id(event)
|
|
18
|
+
else:
|
|
19
|
+
get_session_id = lambda agent, event: "public"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@toolkit.on_skill("工作区工具")
|
|
23
|
+
async def _(agent: CloversAgent, event: Event):
|
|
24
|
+
if not WORKSPACE.exists():
|
|
25
|
+
WORKSPACE.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
if not README.exists():
|
|
27
|
+
README.write_text("Clovers Agent Workspace")
|
|
28
|
+
if not GITIGNORE.exists():
|
|
29
|
+
GITIGNORE.write_text((Path(__file__).parent / "GITIGNORE").read_text())
|
|
30
|
+
if __config__.use_shell:
|
|
31
|
+
session_id = get_session_id(agent, event)
|
|
32
|
+
if session_id in shell_dict:
|
|
33
|
+
shell_dict[session_id].workdir = "/workspace"
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
shell_dict[session_id] = Shell(session_id)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.error(e)
|
|
39
|
+
return f"workspace 已初始化, shell 初始化失败:{e}\n当前工作目录: /workspace"
|
|
40
|
+
return f"workspace 已初始化,当前系统:Debian\n当前工作目录: /workspace"
|
|
41
|
+
else:
|
|
42
|
+
return f"workspace 已初始化\n当前工作目录: /workspace"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __config__.use_shell:
|
|
46
|
+
|
|
47
|
+
@toolkit.tool(
|
|
48
|
+
"shell",
|
|
49
|
+
"在工作区环境下执行命令",
|
|
50
|
+
{"command": {"type": "string", "description": "需要执行的命令,如需要执行多条命令,请使用 `&&` 或 `;`隔开"}},
|
|
51
|
+
["工作区工具"],
|
|
52
|
+
)
|
|
53
|
+
async def _(agent: CloversAgent, event: Event, command: str):
|
|
54
|
+
session_id = get_session_id(agent, event)
|
|
55
|
+
if session_id not in shell_dict:
|
|
56
|
+
return f"Error: shell 初始化失败,请返回故障原因。在故障排除前不要重复调用此方法。"
|
|
57
|
+
shell = shell_dict[session_id]
|
|
58
|
+
logger.info(f"[CloversAgentShell][{session_id}]> {command if (idx := command.find("\n")) == -1 else f"{command[:idx]}..."}")
|
|
59
|
+
output = await shell.execute(command)
|
|
60
|
+
return f"{output}\n当前工作目录: {shell.workdir}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@toolkit.tool(
|
|
64
|
+
"read_files",
|
|
65
|
+
"读取并查看指定文件的内容。支持同时传入多个路径以一次性查看多个文件上下文。"
|
|
66
|
+
"在需要分析代码、检查配置文件时,尤其是需要查看多个文件时,应优先使用此工具以提高效率。",
|
|
67
|
+
{"filepaths": {"type": "array", "description": "包含一个或多个文件路径的数组", "items": {"type": "string"}}},
|
|
68
|
+
["工作区工具"],
|
|
69
|
+
)
|
|
70
|
+
async def _(agent: CloversAgent, event: Event, filepaths: list[str]):
|
|
71
|
+
session_id = get_session_id(agent, event)
|
|
72
|
+
workspace = WORKSPACE / session_id
|
|
73
|
+
md = []
|
|
74
|
+
for file_path in filepaths:
|
|
75
|
+
if file_path.startswith("/workspace"):
|
|
76
|
+
file = workspace / f"./{file_path[10:]}"
|
|
77
|
+
else:
|
|
78
|
+
file = workspace / file_path
|
|
79
|
+
if not file.is_relative_to(workspace):
|
|
80
|
+
md.append(f"{file_path} 非工作区文件")
|
|
81
|
+
if not file.exists():
|
|
82
|
+
md.append(f"{file_path} 文件不存在")
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
content = file.read_text()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"{file_path} 文件读取失败:{e}")
|
|
88
|
+
md.append(f"{file_path} 文件读取失败")
|
|
89
|
+
continue
|
|
90
|
+
md.append(f"```{file_path}\n{content}\n```")
|
|
91
|
+
return "\n\n".join(md)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@toolkit.tool(
|
|
95
|
+
"write_file",
|
|
96
|
+
"写入文件",
|
|
97
|
+
{
|
|
98
|
+
"file_path": {"type": "string", "description": "需要写入的文件路径"},
|
|
99
|
+
"file_content": {"type": "string", "description": "需要写入到文件的内容"},
|
|
100
|
+
},
|
|
101
|
+
["工作区工具"],
|
|
102
|
+
)
|
|
103
|
+
async def _(agent: CloversAgent, event: Event, file_path: str, file_content: str):
|
|
104
|
+
session_id = get_session_id(agent, event)
|
|
105
|
+
workspace = WORKSPACE / session_id
|
|
106
|
+
if file_path.startswith("/workspace"):
|
|
107
|
+
file = workspace / f"./{file_path[10:]}"
|
|
108
|
+
else:
|
|
109
|
+
file = workspace / file_path
|
|
110
|
+
try:
|
|
111
|
+
file.write_text(file_content, encoding="utf-8")
|
|
112
|
+
return f"文件写入成功。"
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(e)
|
|
115
|
+
return f"文件写入失败:{e}"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import docker
|
|
3
|
+
import shlex
|
|
4
|
+
from docker.models.containers import Container
|
|
5
|
+
from docker.types import DeviceRequest
|
|
6
|
+
from docker.errors import NotFound
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
WORKSPACE = Path("workspace")
|
|
11
|
+
client: docker.DockerClient | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Shell:
|
|
15
|
+
|
|
16
|
+
def __init__(self, session_id: str):
|
|
17
|
+
self.lock = asyncio.Lock()
|
|
18
|
+
self.session_id = session_id
|
|
19
|
+
self.workdir = "/workspace"
|
|
20
|
+
self.container: Container | None = None
|
|
21
|
+
global client
|
|
22
|
+
if client is None:
|
|
23
|
+
client = docker.from_env()
|
|
24
|
+
self.client = client
|
|
25
|
+
|
|
26
|
+
async def execute(self, command: str):
|
|
27
|
+
async with self.lock:
|
|
28
|
+
if self.container is None:
|
|
29
|
+
self.workdir = "/workspace"
|
|
30
|
+
workspace = WORKSPACE / self.session_id
|
|
31
|
+
if not workspace.exists():
|
|
32
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
container_name = f"CloversAgentSandbox-{self.session_id}"
|
|
34
|
+
try:
|
|
35
|
+
self.container = await asyncio.to_thread(self.client.containers.get, container_name)
|
|
36
|
+
except NotFound:
|
|
37
|
+
self.container = await asyncio.to_thread(
|
|
38
|
+
self.client.containers.run,
|
|
39
|
+
"nikolaik/python-nodejs:python3.12-nodejs20",
|
|
40
|
+
name=container_name,
|
|
41
|
+
detach=True,
|
|
42
|
+
tty=True,
|
|
43
|
+
command="sleep infinity",
|
|
44
|
+
volumes={workspace.resolve().as_posix(): {"bind": "/workspace", "mode": "rw"}},
|
|
45
|
+
device_requests=[DeviceRequest(count=-1, capabilities=[["gpu"]])],
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
self.container
|
|
49
|
+
self.container.reload()
|
|
50
|
+
if self.container.status != "running":
|
|
51
|
+
await asyncio.to_thread(self.container.start)
|
|
52
|
+
assert self.container is not None
|
|
53
|
+
wrapped_command = f"bash -c {shlex.quote(f"{command}\necho '___CWD_MARKER___'\npwd")}"
|
|
54
|
+
# result = await asyncio.to_thread(self.container.exec_run, wrapped_command, workdir=self.workdir)
|
|
55
|
+
# stdout: str = result.output.decode("utf-8")
|
|
56
|
+
stdout = await asyncio.to_thread(self.execute_thread, wrapped_command)
|
|
57
|
+
output, workdir = stdout.rsplit("___CWD_MARKER___", 1)
|
|
58
|
+
self.workdir = workdir.strip()
|
|
59
|
+
return output
|
|
60
|
+
|
|
61
|
+
async def cleanup(self):
|
|
62
|
+
async with self.lock:
|
|
63
|
+
if self.container is None:
|
|
64
|
+
return
|
|
65
|
+
await asyncio.to_thread(self.container.remove, force=True)
|
|
66
|
+
self.workdir = "/workspace"
|
|
67
|
+
self.container = None
|
|
68
|
+
|
|
69
|
+
def execute_thread(self, command: str):
|
|
70
|
+
"""运行命令,不输出被回车覆盖的行"""
|
|
71
|
+
exec_id = self.client.api.exec_create(self.container.id, command, workdir=self.workdir) # type:ignore
|
|
72
|
+
output_gen = self.client.api.exec_start(exec_id["Id"], stream=True)
|
|
73
|
+
outputs: list[str] = []
|
|
74
|
+
buffer: bytearray = bytearray()
|
|
75
|
+
for chunk in output_gen:
|
|
76
|
+
for byte in chunk:
|
|
77
|
+
buffer.append(byte)
|
|
78
|
+
if byte == 10:
|
|
79
|
+
line = buffer.decode("utf-8")
|
|
80
|
+
# print(line)
|
|
81
|
+
outputs.append(line)
|
|
82
|
+
buffer.clear()
|
|
83
|
+
elif byte == 13:
|
|
84
|
+
# sys.stdout.buffer.write(buffer)
|
|
85
|
+
# sys.stdout.flush()
|
|
86
|
+
buffer.clear()
|
|
87
|
+
if buffer:
|
|
88
|
+
outputs.append(buffer.decode("utf-8"))
|
|
89
|
+
return "\n".join(outputs)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
clovers_agent_toolkit/__init__.py
|
|
4
|
+
clovers_agent_toolkit/config.py
|
|
5
|
+
clovers_agent_toolkit/fetch.py
|
|
6
|
+
clovers_agent_toolkit/toolkit.py
|
|
7
|
+
clovers_agent_toolkit.egg-info/PKG-INFO
|
|
8
|
+
clovers_agent_toolkit.egg-info/SOURCES.txt
|
|
9
|
+
clovers_agent_toolkit.egg-info/dependency_links.txt
|
|
10
|
+
clovers_agent_toolkit.egg-info/requires.txt
|
|
11
|
+
clovers_agent_toolkit.egg-info/top_level.txt
|
|
12
|
+
clovers_agent_toolkit/workspace/__init__.py
|
|
13
|
+
clovers_agent_toolkit/workspace/docker.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clovers_agent_toolkit
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clovers-agent-toolkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [{ name = "KarisAya", email = "karisaya@foxmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12,<4.0.0"
|
|
8
|
+
dependencies = ["clovers-agent>=0.0.4", "docker>=7.1.0"]
|
|
9
|
+
|
|
10
|
+
[tool.uv.sources]
|
|
11
|
+
clovers-agent = { workspace = true, editable = true }
|