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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: clovers-agent-toolkit
3
+ Version: 0.1.0
4
+ Author-email: KarisAya <karisaya@foxmail.com>
5
+ Requires-Python: <4.0.0,>=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: clovers-agent>=0.0.4
8
+ Requires-Dist: docker>=7.1.0
File without changes
@@ -0,0 +1,6 @@
1
+ from .toolkit import toolkit as __plugin__
2
+ from . import fetch as _
3
+ from . import workspace as _
4
+
5
+ __version__ = "0.0.1"
6
+ __all__ = ["__plugin__"]
@@ -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,3 @@
1
+ from clovers_agent import ToolManager
2
+
3
+ toolkit = ToolManager("Agent Toolkit")
@@ -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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: clovers-agent-toolkit
3
+ Version: 0.1.0
4
+ Author-email: KarisAya <karisaya@foxmail.com>
5
+ Requires-Python: <4.0.0,>=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: clovers-agent>=0.0.4
8
+ Requires-Dist: docker>=7.1.0
@@ -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,2 @@
1
+ clovers-agent>=0.0.4
2
+ docker>=7.1.0
@@ -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 }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+