SimAgentPlg 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
allagent/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """All Agent — a lightweight multi-agent framework with ReAct reasoning, tool dispatch, and MCP integration."""
2
+
3
+ from allagent.agent.react.reactor import ReactLoop
4
+ from allagent.agent.chat.chat import ChatLoop
5
+ from allagent.agent.base import LLMConfig, StepOutcome, BaseHandler
6
+ from allagent.plugins.mcp.mcp_manager import McpServerManager
7
+ from allagent.plugins.skill.skill_manager import SkillManager
8
+
9
+ __all__ = [
10
+ "ReactLoop",
11
+ "ChatLoop",
12
+ "LLMConfig",
13
+ "StepOutcome",
14
+ "BaseHandler",
15
+ "McpServerManager",
16
+ "SkillManager",
17
+ ]
@@ -0,0 +1,7 @@
1
+ """Agent 调度 — ReAct / Chat 循环。"""
2
+
3
+ from allagent.agent.react.reactor import ReactLoop
4
+ from allagent.agent.chat.chat import ChatLoop
5
+ from allagent.agent.base import LLMConfig
6
+
7
+ __all__ = ["ReactLoop", "ChatLoop", "LLMConfig"]
allagent/agent/base.py ADDED
@@ -0,0 +1,223 @@
1
+ import asyncio
2
+ from typing import Optional, Any, cast
3
+ import os
4
+
5
+ from dataclasses import dataclass
6
+ from openai import AsyncOpenAI
7
+ from abc import ABC, abstractmethod
8
+ from dotenv import load_dotenv
9
+ from openai.types.chat import ChatCompletionMessage
10
+
11
+ from allagent.logger import get_logger
12
+ from .tool_schema import LOCAL_TOOLS
13
+
14
+ load_dotenv()
15
+
16
+ logger = get_logger("LLMCONFIG")
17
+
18
+ BASE_PROMPT = """
19
+ 你是一个帮助用户完成各种任务的聊天助手
20
+ """
21
+
22
+
23
+ @dataclass
24
+ class StepOutcome:
25
+ data: Any # 工具返回值
26
+ next_prompt: Optional[str] = None # 下一轮追加的 prompt,None 表示任务完成
27
+ should_exit: bool = False # True 表示立即退出
28
+
29
+
30
+ # bash 命令黑名单
31
+ BASH_BLACKLIST = [
32
+ "rm ",
33
+ "rm\n",
34
+ "rm\t",
35
+ "rm(",
36
+ "rm;",
37
+ "rm\\",
38
+ "rm|",
39
+ "rm&",
40
+ "rm<",
41
+ "rm>",
42
+ "sudo ",
43
+ "mkfs.",
44
+ "dd if=",
45
+ ":(){ :|:& };:",
46
+ "> /dev/sda",
47
+ "/dev/null",
48
+ "chmod 777",
49
+ ]
50
+
51
+
52
+ async def bash_run(
53
+ code: str,
54
+ timeout: int = 60,
55
+ cwd: Optional[str] = None,
56
+ maxlen: int = 10000,
57
+ ) -> dict:
58
+ """异步执行 bash 代码片段,返回执行结果 dict。"""
59
+ logger.info("bash_run 脚本:\n%s...", code[:40])
60
+
61
+ for pattern in BASH_BLACKLIST:
62
+ if pattern in code:
63
+ logger.warning("bash_run 命中黑名单: %s", pattern.strip())
64
+ return {
65
+ "status": "error",
66
+ "msg": f"禁止执行危险命令: {pattern.strip()}",
67
+ "exit_code": -1,
68
+ }
69
+
70
+ try:
71
+ process = await asyncio.create_subprocess_exec(
72
+ "bash",
73
+ "-c",
74
+ code,
75
+ stdout=asyncio.subprocess.PIPE,
76
+ stderr=asyncio.subprocess.STDOUT,
77
+ cwd=cwd,
78
+ )
79
+
80
+ stdout_chunks: list[str] = []
81
+
82
+ async def read_stdout() -> None:
83
+ assert process.stdout is not None
84
+ while True:
85
+ line_bytes = await process.stdout.readline()
86
+ if not line_bytes:
87
+ break
88
+ try:
89
+ line = line_bytes.decode("utf-8")
90
+ except UnicodeDecodeError:
91
+ line = line_bytes.decode("gbk", errors="ignore")
92
+ stdout_chunks.append(line)
93
+
94
+ read_task = asyncio.create_task(read_stdout())
95
+
96
+ try:
97
+ await asyncio.wait_for(process.wait(), timeout=timeout)
98
+ except asyncio.TimeoutError:
99
+ try:
100
+ process.kill()
101
+ except Exception:
102
+ pass
103
+ await process.wait()
104
+ stdout_chunks.append("\n[Timeout Error] 超时强制终止\n")
105
+
106
+ await read_task
107
+
108
+ stdout_str = "".join(stdout_chunks)
109
+ exit_code = process.returncode if process.returncode is not None else -1
110
+ status = "success" if exit_code == 0 else "error"
111
+
112
+ return {
113
+ "status": status,
114
+ "stdout": stdout_str[-maxlen:],
115
+ "exit_code": exit_code,
116
+ }
117
+
118
+ except Exception as e:
119
+ return {"status": "error", "msg": str(e)}
120
+
121
+
122
+ class BaseHandler:
123
+ """
124
+ 工具调度基类 —— 约定优于配置:
125
+ 子类只需定义 do_{tool_name} 方法,LLM 调用该工具时会自动反射路由。
126
+ """
127
+
128
+ async def dispatch(
129
+ self, tool_name: str, args: dict, index: int = 0, tool_num: int = 1
130
+ ) -> StepOutcome:
131
+ """
132
+ 根据 tool_name 反射到 self.do_{tool_name} 方法。
133
+ 自动注入 _index / _tool_num 参数。
134
+ """
135
+ method_name = f"do_{tool_name}"
136
+ if hasattr(self, method_name):
137
+ args["_index"] = index
138
+ args["_tool_num"] = tool_num
139
+ return await getattr(self, method_name)(args)
140
+ else:
141
+ return StepOutcome(
142
+ None, next_prompt=f"未知工具 {tool_name}", should_exit=False
143
+ )
144
+
145
+
146
+ class LLMConfig(BaseHandler, ABC):
147
+ """
148
+ 为后续ReAct, Plan and Execute,提供基础框架
149
+ 它用于调用任何兼容OpenAI接口的服务,并默认使用流式响应。
150
+ """
151
+
152
+ def __init__(self, temperature: float = 0.7):
153
+ """
154
+ 初始化客户端。参数从环境变量加载。
155
+ """
156
+ model = os.getenv("CHAT_MODEL")
157
+ api_key = os.getenv("MODEL_API_KEY")
158
+ base_url = os.getenv("MODEL_URL")
159
+ timeout = int(os.getenv("LLM_TIMEOUT", 60))
160
+ self.temperature = temperature
161
+
162
+ if not model or not api_key or not base_url:
163
+ raise ValueError("模型ID、API密钥和服务地址必须被提供或在.env文件中定义。")
164
+
165
+ self.model = model
166
+ self.apiKey = api_key
167
+ self.baseUrl = base_url
168
+ self.timeout = timeout
169
+ self.exec_cwd = os.getcwd()
170
+ self.messages: list = []
171
+ self.all_tools = [*LOCAL_TOOLS]
172
+ self.client = AsyncOpenAI(api_key=api_key, base_url=base_url, timeout=timeout)
173
+
174
+ @abstractmethod
175
+ async def runtime(
176
+ self, *, task: str, system_prompt: str = BASE_PROMPT,
177
+ history: Optional[list[dict]] = None,
178
+ ) -> str | None:
179
+ pass
180
+
181
+ async def chat_text(
182
+ self, messages: list[dict[str, str]], *, tools: Optional[list[dict[str, str]]]
183
+ ) -> ChatCompletionMessage:
184
+ """Call the configured chat model and return stripped text."""
185
+
186
+ try:
187
+ response = await self.client.chat.completions.create(
188
+ model=self.model,
189
+ messages=cast(Any, messages),
190
+ temperature=self.temperature,
191
+ tools=tools, # ty:ignore[invalid-argument-type]
192
+ )
193
+ except Exception as exc:
194
+ raise KeyError(f"chat completion failed: {exc}") from exc
195
+ message: ChatCompletionMessage = response.choices[0].message
196
+ return message
197
+
198
+ async def do_bash_run(self, args: dict) -> StepOutcome:
199
+ """执行 bash 代码片段。"""
200
+ code = args.get("code") or args.get("script")
201
+ if not code:
202
+ logger.warning("bash_run 缺少 code 参数")
203
+ return StepOutcome(
204
+ "[Error] Code missing. Use 'code' or 'script' arg.",
205
+ next_prompt="\n",
206
+ )
207
+
208
+ try:
209
+ timeout = int(args.get("timeout", 60))
210
+ except Exception:
211
+ timeout = 60
212
+
213
+ tool_num = args.get("_tool_num", 1)
214
+ maxlen = max(1, 10000 // tool_num)
215
+
216
+ logger.info("执行 bash_run, timeout=%d, cwd=%s", timeout, self.exec_cwd)
217
+ result = await bash_run(code, timeout=timeout, cwd=self.exec_cwd, maxlen=maxlen)
218
+ logger.info(
219
+ "bash_run 完成, status=%s, exit_code=%s",
220
+ result.get("status"),
221
+ result.get("exit_code"),
222
+ )
223
+ return StepOutcome(result, next_prompt="\n")
@@ -0,0 +1,56 @@
1
+ from allagent.logger import get_logger
2
+
3
+ from typing import Optional
4
+
5
+ from allagent.agent.base import LLMConfig
6
+
7
+ logger = get_logger("CHATAGENT")
8
+
9
+ CHAT_PROMPT = """
10
+ 现在你的身份是我的同龄好朋友,我们日常随意聊天。
11
+ 说话接地气、幽默风趣,偶尔开玩笑,不用正式话术。
12
+ 可以一起聊生活、兴趣、美食、趣事,接话自然流畅,不要像机器人。
13
+ """
14
+
15
+ MAX_STEP = 5
16
+
17
+
18
+ class ChatLoop(LLMConfig):
19
+
20
+ def __init__(
21
+ self,
22
+ ) -> None:
23
+ super().__init__()
24
+
25
+ async def runtime(
26
+ self,
27
+ *,
28
+ task: str,
29
+ system_prompt: str = CHAT_PROMPT,
30
+ history: Optional[list[dict]] = None,
31
+ ) -> str | None:
32
+
33
+ self.messages = [{"role": "system", "content": system_prompt}]
34
+ if history:
35
+ self.messages.extend(history)
36
+ self.messages.append({"role": "user", "content": task})
37
+
38
+ for turn in range(MAX_STEP):
39
+ logger.info("第 %d/%d 轮", turn + 1, MAX_STEP)
40
+
41
+ message = await self.chat_text(self.messages, tools=None)
42
+ self.messages.append(message.model_dump())
43
+ return message.content
44
+
45
+
46
+ async def main():
47
+ task = "你好介绍一下自己"
48
+ loop = ChatLoop()
49
+ result = await loop.runtime(task=task)
50
+ logger.info("Chat 运行结果: %s", result)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ import asyncio
55
+
56
+ asyncio.run(main())
@@ -0,0 +1,10 @@
1
+ {
2
+ "playwright": {
3
+ "command": "npx",
4
+ "args": [
5
+ "@playwright/mcp@latest",
6
+ "--headless",
7
+ "--browser=chrome"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: 查询天气的免费网站
3
+ description: 通过多个免费天气网站查询实时天气和预报
4
+ ---
5
+
6
+ # 天气查询 Skill
7
+
8
+ 本 Skill 集成了以下免费的天气查询服务,可直接通过 HTTP 请求获取天气信息。
9
+
10
+ ## 支持的免费天气网站
11
+
12
+ ### 1. wttr.in
13
+ - **网址**:https://wttr.in
14
+ - **说明**:命令行风格的天气服务,支持 `curl wttr.in/城市名` 返回简洁的文本天气信息。
15
+ - **特点**:无需 API Key,支持中英文,可自定义输出格式(如 `?format="%c+%t"`)。
16
+
17
+ ### 2. Open-Meteo
18
+ - **网址**:https://open-meteo.com
19
+ - **API 端点**:`https://api.open-meteo.com/v1/forecast`
20
+ - **说明**:免费、开源的天气 API,返回 JSON 格式的预报和实时数据。
21
+ - **特点**:无需注册,支持全球任意经纬度查询,最多 16 天预报。
22
+
23
+ ### 3. NOAA Weather API (美国)
24
+ - **网址**:https://www.weather.gov/documentation/services-web-api
25
+ - **说明**:美国国家气象局官方 API,提供权威的天气、预警和观测数据。
26
+ - **特点**:完全免费,无需 API Key,但建议设置 User-Agent 标识。
27
+
28
+ ### 4. 7Timer!
29
+ - **网址**:https://www.7timer.info
30
+ - **API 端点**:`http://www.7timer.info/bin/api.pl`
31
+ - **说明**:提供气象学和天文学天气数据,支持全球查询。
32
+ - **特点**:免费,无需 API Key,返回 JSON 或 XML 格式。
33
+
34
+ ### 5. WeatherAPI (免费层级)
35
+ - **网址**:https://www.weatherapi.com
36
+ - **说明**:提供免费层级的 API(每天 100 万次调用,需注册获取 API Key)。
37
+ - **特点**:实时天气、预报、历史数据,免费版足够个人使用。
38
+
39
+ ## 使用建议
40
+
41
+ - 快速查询推荐 **wttr.in**,无需解析 JSON。
42
+ - 需要结构化数据推荐 **Open-Meteo**,完全免费且功能强大。
43
+ - 美国区域查询推荐 **NOAA API**,数据权威。
@@ -0,0 +1,123 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from allagent.logger import get_logger
5
+
6
+ from typing import Optional
7
+
8
+ from allagent.plugins import McpServerManager, SkillManager
9
+ from allagent.agent.base import LLMConfig, StepOutcome
10
+
11
+ logger = get_logger("REACTAGENT")
12
+
13
+ REACT_LOOP_PROMPT = """
14
+ 你是一个有能力调用外部工具的智能助手。你必须严格遵循以下 ReAct 流程:
15
+
16
+ 1. Thought: 分析当前问题,规划下一步行动。
17
+ 2. Action: 调用一个工具来执行行动。
18
+ - 如果需要操作浏览器/文件系统/技能,调用对应的 MCP 工具或技能工具。
19
+ 重要规则:
20
+ - 每轮只能调用一个或一组工具,不能同时输出思考内容和工具调用之外的文字。
21
+ - 工具执行结果会返回给你,请根据结果继续思考下一步。
22
+ - 不要重复相同的无效操作。
23
+ """
24
+
25
+ MAX_STEP = 20
26
+
27
+
28
+ class ReactLoop(LLMConfig):
29
+
30
+ def __init__(
31
+ self,
32
+ ) -> None:
33
+ super().__init__()
34
+ self._startup: bool = False
35
+ _agent_dir = Path(__file__).parent
36
+ self.mcp_manager: McpServerManager = McpServerManager(
37
+ _agent_dir / "mcp_config.json"
38
+ )
39
+ self.skill_manager: SkillManager = SkillManager(_agent_dir / "react_skill")
40
+
41
+ async def dispatch(
42
+ self, tool_name: str, args: dict, index: int = 0, tool_num: int = 1
43
+ ) -> StepOutcome:
44
+ outcome = await super().dispatch(tool_name, args, index, tool_num)
45
+ if outcome.next_prompt and outcome.next_prompt.startswith("未知工具"):
46
+ result = await self.mcp_manager.call_tool(tool_name, args)
47
+ return StepOutcome(data=result)
48
+ return outcome
49
+
50
+ async def startup(self) -> None:
51
+ await self.mcp_manager.startup()
52
+ mcp_tools = self.mcp_manager.get_openai_tools()
53
+ self.all_tools.extend(mcp_tools)
54
+ await self.skill_manager.discover()
55
+
56
+ async def runtime(
57
+ self, *, task: str, system_prompt: str = REACT_LOOP_PROMPT,
58
+ history: Optional[list[dict]] = None,
59
+ ) -> str | None:
60
+
61
+ if self._startup is False:
62
+ await self.startup()
63
+ self._startup = True
64
+
65
+ self.messages = [{"role": "system", "content": system_prompt}]
66
+ if history:
67
+ self.messages.extend(history)
68
+ self.messages.append({"role": "user", "content": task})
69
+
70
+ last_skill_name: str | None = None
71
+
72
+ for turn in range(MAX_STEP):
73
+ logger.info("第 %d/%d 轮", turn + 1, MAX_STEP)
74
+
75
+ skill_dispatch = await self.skill_manager.dispatch(self.messages)
76
+
77
+ if skill_dispatch:
78
+ skill_name = skill_dispatch.get("skill_name", "")
79
+ if skill_name and skill_name != last_skill_name:
80
+ last_skill_name = skill_name
81
+ self.messages.extend(skill_dispatch["messages"])
82
+
83
+ message = await self.chat_text(self.messages, tools=self.all_tools)
84
+ self.messages.append(message.model_dump())
85
+
86
+ if not message.tool_calls:
87
+ if message.content:
88
+ return message.content # 普通文字回复直接返回
89
+ continue
90
+
91
+ fn_calls = [tc for tc in message.tool_calls if tc.type == "function"]
92
+
93
+ for ii, tc in enumerate(fn_calls):
94
+ tool_args = json.loads(tc.function.arguments)
95
+ outcome = await self.dispatch(
96
+ tc.function.name, tool_args, index=ii, tool_num=len(fn_calls)
97
+ )
98
+
99
+ if outcome.should_exit:
100
+ return outcome.data
101
+ if outcome.next_prompt is None:
102
+ break
103
+
104
+ self.messages.append(
105
+ {
106
+ "role": "tool",
107
+ "tool_call_id": tc.id,
108
+ "content": str(outcome.data),
109
+ }
110
+ )
111
+
112
+
113
+ async def main():
114
+ task = "写一个python脚本保存到本地,主要是打印helloworld并测试它,测试完成后删除"
115
+ loop = ReactLoop()
116
+ result = await loop.runtime(task=task)
117
+ logger.info("ReAct 运行结果: %s", result)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ import asyncio
122
+
123
+ asyncio.run(main())
@@ -0,0 +1,29 @@
1
+ """
2
+ 本地工具 schema —— 纯数据定义,LLMConfig 默认可用的所有本地工具。
3
+
4
+ 添加新工具只需追加一条 dict,同时在 LLMConfig 中定义对应的 do_xxx 方法。
5
+ """
6
+
7
+ LOCAL_TOOLS: list[dict] = [
8
+ {
9
+ "type": "function",
10
+ "function": {
11
+ "name": "bash_run",
12
+ "description": "在 Bash 沙箱中执行一段脚本代码,返回 stdout、退出码和状态。用于运行命令行、操作文件、安装依赖等操作。",
13
+ "parameters": {
14
+ "type": "object",
15
+ "properties": {
16
+ "code": {
17
+ "type": "string",
18
+ "description": "要执行的 Bash 脚本代码,支持多行",
19
+ },
20
+ "timeout": {
21
+ "type": "integer",
22
+ "description": "最长等待秒数,默认 60",
23
+ },
24
+ },
25
+ "required": ["code"],
26
+ },
27
+ },
28
+ },
29
+ ]
allagent/logger.py ADDED
@@ -0,0 +1,46 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger(
5
+ name: str = "All-Agent",
6
+ level: str = "INFO",
7
+ log_file: str = "./logs/app.log",
8
+ fmt: str = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
9
+ ):
10
+ """创建并配置日志记录器,输出到控制台。
11
+
12
+ Args:
13
+ name: 日志记录器名称。
14
+ level: 最低日志级别,默认为 "INFO"。
15
+ log_file: 日志文件路径(当前仅保留参数,暂未启用文件输出)。
16
+ fmt: 日志格式字符串。
17
+
18
+ Returns:
19
+ 配置完成的 logging.Logger 实例。
20
+ """
21
+ logger = logging.getLogger(name)
22
+ logger.setLevel(level)
23
+
24
+ if logger.handlers:
25
+ return logger
26
+
27
+ formatter = logging.Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S")
28
+
29
+ console_handler = logging.StreamHandler()
30
+ console_handler.setLevel(level)
31
+ console_handler.setFormatter(formatter)
32
+ logger.addHandler(console_handler)
33
+
34
+ return logger
35
+
36
+
37
+ def get_logger(name: str = "All-Agent") -> logging.Logger:
38
+ """获取或创建指定名称的日志记录器。
39
+
40
+ Args:
41
+ name: 日志记录器名称,默认为 "All-Agent"。
42
+
43
+ Returns:
44
+ 配置完成的 logging.Logger 实例。
45
+ """
46
+ return setup_logger(name)
@@ -0,0 +1,6 @@
1
+ """插件系统 — MCP 服务管理 & Skill 技能路由。"""
2
+
3
+ from allagent.plugins.mcp.mcp_manager import McpServerManager
4
+ from allagent.plugins.skill.skill_manager import SkillManager
5
+
6
+ __all__ = ["McpServerManager", "SkillManager"]
@@ -0,0 +1,118 @@
1
+ import json
2
+ import asyncio
3
+ from pathlib import Path
4
+ from fastmcp import Client
5
+ from fastmcp.mcp_config import MCPConfig
6
+ from mcp.types import Tool
7
+ from allagent.logger import get_logger
8
+
9
+ logger = get_logger(name="MCP")
10
+
11
+
12
+ class McpServerManager:
13
+ """MCP 多服务管理器,负责从 JSON 配置加载、连接和管理多个 MCP 服务。
14
+
15
+ 支持按服务名前缀路由工具调用,单个服务连接失败不影响其他服务。
16
+ """
17
+
18
+ def __init__(self, path: Path | None = None):
19
+ """初始化管理器。
20
+
21
+ Args:
22
+ path: MCP 配置 JSON 文件路径。
23
+ """
24
+ if path is None:
25
+ path = Path(__file__).parent / "mcp_config.json"
26
+ self.path = path
27
+ self.mcp_clients_map: dict[str, Client] = {}
28
+ self.mcp_tools_map: dict[str, list[Tool]] = {}
29
+
30
+ async def startup(self) -> None:
31
+ """启动所有 MCP 服务连接。
32
+
33
+ 从 JSON 配置文件读取服务列表,逐个建立连接并拉取工具列表。
34
+ 单个服务连接失败会记录错误日志但不阻断其他服务的启动。
35
+ """
36
+ logger.info("MCP 服务管理器启动中...")
37
+ with open(self.path, "r", encoding="utf-8") as f:
38
+ mcp_configs = MCPConfig.from_dict(json.load(f))
39
+ logger.info(f"加载到 {len(mcp_configs.mcpServers)} 个 MCP 服务配置")
40
+ for service_name, server_model in mcp_configs.mcpServers.items():
41
+ try:
42
+ logger.info(f"正在连接 {service_name} ...")
43
+ mcp_client = Client({service_name: server_model.model_dump()})
44
+ await mcp_client.__aenter__()
45
+ tools = await mcp_client.list_tools()
46
+ self.mcp_tools_map[service_name] = tools
47
+ self.mcp_clients_map[service_name] = mcp_client
48
+ logger.info(f"{service_name} 连接成功,加载 {len(tools)} 个工具")
49
+ except Exception as e:
50
+ logger.error(f"连接 {service_name} 失败: {e}")
51
+ logger.info(
52
+ f"MCP 服务管理器启动完成,共 {len(self.mcp_clients_map)} 个服务在线"
53
+ )
54
+
55
+ async def shutdown(self) -> None:
56
+ """关闭所有 MCP 服务连接,释放资源。"""
57
+ logger.info("MCP 服务管理器关闭中...")
58
+ for service_name, mcp_client in self.mcp_clients_map.items():
59
+ await mcp_client.__aexit__(None, None, None)
60
+ logger.info(f"{service_name} 已断开")
61
+ logger.info("MCP 服务管理器已关闭")
62
+
63
+ async def call_tool(self, tool_name: str, args: dict[str, str]) -> str:
64
+ """调用 MCP 工具,按服务名前缀自动路由。
65
+
66
+ Args:
67
+ tool_name: 工具名,格式为 "{服务名}__{工具名}",如 "playwright__browser_navigate"。
68
+ args: 传递给工具的参数字典。
69
+
70
+ Returns:
71
+ 工具执行结果的字符串表示。
72
+
73
+ Raises:
74
+ ValueError: 当工具名不存在于任何已连接的服务中时抛出。
75
+ """
76
+ logger.info(f"调用工具: {tool_name}, 参数: {args}")
77
+ for service_name, client in self.mcp_clients_map.items():
78
+ prefix = f"{service_name}__"
79
+ if tool_name.startswith(prefix):
80
+ raw_name = tool_name[len(prefix) :]
81
+ result = await client.call_tool(raw_name, args)
82
+ logger.info(f"工具 {tool_name} 执行成功")
83
+ return str(result)
84
+ logger.error(f"未知的 MCP 工具: {tool_name}")
85
+ raise ValueError(f"unknown MCP tool: {tool_name}")
86
+
87
+ def get_openai_tools(self) -> list[dict]:
88
+ """将所有已连接服务的工具转换为 OpenAI tools 格式。
89
+
90
+ Returns:
91
+ OpenAI tools 参数格式的列表。
92
+ """
93
+ openai_tools = []
94
+ for service_name, tools in self.mcp_tools_map.items():
95
+ for tool in tools:
96
+ openai_tools.append(
97
+ {
98
+ "type": "function",
99
+ "function": {
100
+ "name": f"{service_name}__{tool.name}",
101
+ "description": tool.description,
102
+ "parameters": tool.inputSchema,
103
+ },
104
+ }
105
+ )
106
+ logger.info(f"转换到 OpenAI tools 格式的工具: {len(openai_tools)} 个工具")
107
+ return openai_tools
108
+
109
+
110
+ async def main():
111
+ mcp_manager = McpServerManager()
112
+ await mcp_manager.startup()
113
+ mcp_manager.get_openai_tools()
114
+ await mcp_manager.shutdown()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ asyncio.run(main())
@@ -0,0 +1,246 @@
1
+ from typing import Optional
2
+ import json
3
+ import os
4
+ import re
5
+ import yaml
6
+
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from dotenv import load_dotenv
10
+ from openai import OpenAI
11
+ from allagent.logger import get_logger
12
+
13
+ load_dotenv()
14
+ logger = get_logger("skill")
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Skill:
19
+ name: str
20
+ root: Path
21
+ skill_md: Path
22
+ template_md: Path | None = None
23
+ sample_md: Path | None = None
24
+
25
+
26
+ class SkillManager:
27
+ """技能注册表,扫描本地技能目录并通过 LLM 路由用户请求到匹配的技能。
28
+
29
+ 核心流程:
30
+ 1. discover() — 扫描 skills_root 下含 SKILL.md 的子目录,注册技能
31
+ 2. dispatch() — 用 LLM 从自然语言输入中选择技能并构建 messages
32
+ 3. _build_messages() — 将技能定义、模板、示例拼装为 LLM 对话格式
33
+ """
34
+
35
+ def __init__(self, skills_root: str | Path | None = None):
36
+ """
37
+ Args:
38
+ skills_root: 技能根目录路径,子目录中含 SKILL.md 的会被识别为技能。
39
+ """
40
+ if skills_root is None:
41
+ skills_root = Path(__file__).parent / "my_skills"
42
+ self.skills_root = Path(skills_root)
43
+ self._skills: dict[str, Skill] = {}
44
+ self._discovered: bool = False
45
+ logger.info("技能注册表初始化,根目录: %s", self.skills_root)
46
+
47
+ async def discover(self) -> None:
48
+ """扫描技能根目录,注册所有含 SKILL.md 的子目录为技能。
49
+
50
+ Returns:
51
+ 注册的技能字典 {name: Skill}。
52
+
53
+ Raises:
54
+ FileNotFoundError: 技能根目录不存在。
55
+ """
56
+
57
+ if not self.skills_root.exists():
58
+ raise FileNotFoundError(f"skills root not found: {self.skills_root}")
59
+
60
+ self._skills.clear()
61
+
62
+ for child in sorted(self.skills_root.iterdir()):
63
+ if not child.is_dir():
64
+ continue
65
+
66
+ skill_md = child / "SKILL.md"
67
+ if not skill_md.exists():
68
+ continue
69
+
70
+ template_md = child / "template.md"
71
+ sample_md = child / "examples" / "sample.md"
72
+
73
+ self._skills[child.name] = Skill(
74
+ name=child.name,
75
+ root=child,
76
+ skill_md=skill_md,
77
+ template_md=template_md if template_md.exists() else None,
78
+ sample_md=sample_md if sample_md.exists() else None,
79
+ )
80
+
81
+ if not self._skills:
82
+ logger.debug(
83
+ "技能扫描完成,未发现任何技能(skills_root: %s)", self.skills_root
84
+ )
85
+ else:
86
+ logger.info(
87
+ "发现 %d 个技能: %s", len(self._skills), list(self._skills.keys())
88
+ )
89
+
90
+ async def dispatch(
91
+ self,
92
+ messages: list[dict],
93
+ ) -> Optional[dict]:
94
+ """用 LLM 根据对话历史自动匹配技能并构建对话消息。
95
+
96
+ Args:
97
+ messages: 当前对话历史消息列表。
98
+
99
+ Returns:
100
+ if not self._discovered:
101
+ 包含 skill_name、task、me
102
+ self._discovered = True
103
+ if not self._skills:
104
+ # 启动时已经扫过一次,仍为空:直接跳过 LLM 路由,避免无意义 IO + 同步阻塞
105
+ return Nonessages 的 payload 字典。
106
+ 若无匹配返回 None。
107
+ """
108
+ if not self._skills:
109
+ await self.discover()
110
+
111
+ client = OpenAI(
112
+ api_key=os.environ["MODEL_API_KEY"],
113
+ base_url=os.environ["MODEL_URL"],
114
+ )
115
+
116
+ def _read_skill_frontmatter(skill: Skill) -> dict[str, str]:
117
+ """解析 SKILL.md 的 YAML 头部。"""
118
+ text = skill.skill_md.read_text(encoding="utf-8")
119
+ match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
120
+ if match:
121
+ return yaml.safe_load(match.group(1))
122
+ return {}
123
+
124
+ skill_list: str = "\n".join(
125
+ f"- {name}: {_read_skill_frontmatter(skill)}"
126
+ for name, skill in self._skills.items()
127
+ )
128
+
129
+ # 提取原始任务和最新进展,构建紧凑上下文
130
+ task = ""
131
+ latest = ""
132
+ for m in reversed(messages):
133
+ if m.get("role") == "assistant" and not latest:
134
+ latest = m.get("content", "")
135
+ if m.get("role") == "user" and not task:
136
+ task = m.get("content", "")
137
+
138
+ context = f"原始任务: {task}"
139
+ if latest:
140
+ context += f"\n当前进展: {latest[:300]}"
141
+
142
+ response = client.chat.completions.create(
143
+ model=os.environ.get("SKILL_MODEL", "gpt-4o-mini"),
144
+ messages=[
145
+ {
146
+ "role": "system",
147
+ "content": (
148
+ "你是一个技能路由器。根据用户输入和当前进展,选择最匹配的技能并提取任务描述。\n"
149
+ "可用技能列表:\n" + skill_list
150
+ ),
151
+ },
152
+ {"role": "user", "content": context},
153
+ ],
154
+ tools=[
155
+ {
156
+ "type": "function",
157
+ "function": {
158
+ "name": "select_skill",
159
+ "description": "选择要使用的技能并提取任务",
160
+ "parameters": {
161
+ "type": "object",
162
+ "properties": {
163
+ "skill_name": {
164
+ "type": "string",
165
+ "enum": list(self._skills.keys()),
166
+ "description": "选择的技能名",
167
+ },
168
+ "task": {
169
+ "type": "string",
170
+ "description": "用户想要执行的具体任务",
171
+ },
172
+ },
173
+ "required": ["skill_name", "task"],
174
+ },
175
+ },
176
+ }
177
+ ],
178
+ )
179
+
180
+ msg = response.choices[0].message
181
+ if not msg.tool_calls:
182
+ return None
183
+
184
+ args = json.loads(
185
+ msg.tool_calls[0].function.arguments # ty:ignore[unresolved-attribute]
186
+ )
187
+
188
+ skill_name: str = args.get("skill_name", "")
189
+ task: str = args.get("task", "")
190
+
191
+ try:
192
+ skill = self._skills[skill_name]
193
+ except KeyError:
194
+ logger.warning(
195
+ "LLM 返回了未知技能: %s,可用: %s,跳过本次路由",
196
+ skill_name,
197
+ list(self._skills.keys()),
198
+ )
199
+ return None
200
+
201
+ logger.info(f"LLM 路由结果 — 技能: {skill_name}, 任务: {task}")
202
+ return {
203
+ "skill_name": skill.name,
204
+ "task": task,
205
+ "messages": self._build_messages(skill, task),
206
+ }
207
+
208
+ def _build_messages(self, skill: Skill, task: str) -> list[dict[str, str]]:
209
+ """将技能定义、模板和示例拼装为 LLM 对话消息。
210
+
211
+ Args:
212
+ skill: 技能对象。
213
+ task: 用户任务描述。
214
+
215
+ Returns:
216
+ [system_message, user_message] 格式的消息列表。
217
+ """
218
+ system_parts = [
219
+ f'You are executing the local skill "{skill.name}".',
220
+ "",
221
+ "[SKILL.md]",
222
+ skill.skill_md.read_text(encoding="utf-8").strip(),
223
+ ]
224
+
225
+ if skill.template_md is not None:
226
+ system_parts.extend(
227
+ [
228
+ "",
229
+ "[template.md]",
230
+ skill.template_md.read_text(encoding="utf-8").strip(),
231
+ ]
232
+ )
233
+
234
+ if skill.sample_md is not None:
235
+ system_parts.extend(
236
+ [
237
+ "",
238
+ "[examples/sample.md]",
239
+ skill.sample_md.read_text(encoding="utf-8").strip(),
240
+ ]
241
+ )
242
+
243
+ return [
244
+ {"role": "system", "content": "\n".join(system_parts)},
245
+ {"role": "user", "content": task},
246
+ ]
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: SimAgentPlg
3
+ Version: 0.1.0
4
+ Summary: A lightweight multi-agent framework with ReAct reasoning, tool dispatch, and MCP integration
5
+ License: MIT License
6
+
7
+ Copyright (c) 2024
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE
27
+ Requires-Python: >=3.12
28
+ Requires-Dist: fastmcp>=3.4.2
29
+ Requires-Dist: openai>=2.41.0
30
+ Requires-Dist: python-dotenv>=1.2.2
31
+ Description-Content-Type: text/markdown
32
+
33
+ # All Agent
34
+
35
+ A lightweight multi-agent framework with ReAct reasoning, tool dispatch, and MCP integration.
36
+
37
+ ## Features
38
+
39
+ - **ReAct Agent** — ReAct (Reasoning + Acting) loop with multi-turn tool calling
40
+ - **Chat Agent** — simple conversational agent with multi-turn history support
41
+ - **Tool Dispatch** — convention-over-configuration: define `do_{tool_name}` methods, auto-routed via reflection
42
+ - **MCP Integration** — pluggable MCP server manager for external tool providers
43
+ - **Skill System** — skill-based prompt injection for domain-specific behaviors
44
+ - **Built-in Bash Executor** — async sandboxed bash execution with timeout, output truncation, and blacklist filtering
45
+ - **Stateless Execution** — each `runtime()` call starts with a clean context; history is caller-managed
46
+ - **OpenAI-compatible** — works with any OpenAI-compatible API (DeepSeek, etc.)
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install all-agent
52
+ ```
53
+
54
+ Or with uv:
55
+
56
+ ```bash
57
+ uv pip install all-agent
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ Set up your environment variables (`.env`):
63
+
64
+ ```env
65
+ CHAT_MODEL=deepseek-chat
66
+ MODEL_API_KEY=sk-xxxxxxxx
67
+ MODEL_URL=https://api.deepseek.com
68
+ LLM_TIMEOUT=30
69
+ ```
70
+
71
+ ### Chat Agent
72
+
73
+ ```python
74
+ from allagent import ChatLoop
75
+
76
+ loop = ChatLoop()
77
+ result = await loop.runtime(task="介绍一下你自己")
78
+
79
+ # With multi-turn history
80
+ history = [
81
+ {"role": "user", "content": "今天天气不错"},
82
+ {"role": "assistant", "content": "是啊,适合出去走走"},
83
+ ]
84
+ result = await loop.runtime(task="我们去哪", history=history)
85
+ ```
86
+
87
+ ### ReAct Agent
88
+
89
+ ```python
90
+ from allagent import ReactLoop
91
+
92
+ loop = ReactLoop()
93
+ result = await loop.runtime(task="帮我写一个Python脚本打印当前时间")
94
+ ```
95
+
96
+ The ReAct agent supports built-in tools (like `bash_run`) and any MCP tools configured in `mcp_config.json`.
97
+
98
+ ### MCP Configuration
99
+
100
+ Place an `mcp_config.json` alongside your ReactLoop:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "playwright": {
106
+ "command": "npx",
107
+ "args": ["-y", "@anthropic/mcp-playwright"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ## Architecture
114
+
115
+ ```
116
+ LLMConfig (BaseHandler, ABC)
117
+ ├── ChatLoop — stateless conversational agent
118
+ ├── ReactLoop — ReAct reasoning + tool dispatch
119
+ │ ├── MCP tools — external tools via MCP protocol
120
+ │ ├── Skill system — domain-specific prompt injection
121
+ │ └── Local tools — built-in bash_run, extensible
122
+ └── (future) PlanLoop / ExecuteLoop
123
+ ```
124
+
125
+ ### Tool Dispatch Flow
126
+
127
+ ```
128
+ LLM calls "bash_run"
129
+ → BaseHandler.dispatch("bash_run", args)
130
+ → hasattr(self, "do_bash_run")? YES
131
+ → await self.do_bash_run(args) ← local tool
132
+ → NO
133
+ → "未知工具" → MCP fallback ← external tool
134
+ ```
135
+
136
+ ### Adding a Local Tool
137
+
138
+ 1. Define the tool schema in `tool_schema.py`:
139
+
140
+ ```python
141
+ LOCAL_TOOLS = [
142
+ {
143
+ "type": "function",
144
+ "function": {
145
+ "name": "calculator",
146
+ "description": "Evaluate a math expression",
147
+ "parameters": {
148
+ "type": "object",
149
+ "properties": {
150
+ "expression": {"type": "string", "description": "Math expression"}
151
+ },
152
+ "required": ["expression"]
153
+ }
154
+ }
155
+ }
156
+ ]
157
+ ```
158
+
159
+ 2. Add the `do_calculator` method in `LLMConfig`:
160
+
161
+ ```python
162
+ async def do_calculator(self, args: dict) -> StepOutcome:
163
+ result = eval(args["expression"])
164
+ return StepOutcome(data=result, next_prompt="\n")
165
+ ```
166
+
167
+ All agents automatically inherit the new tool.
168
+
169
+ ## API
170
+
171
+ ### `ChatLoop`
172
+
173
+ ```python
174
+ loop = ChatLoop(temperature=0.7)
175
+ await loop.runtime(*, task, system_prompt=None, history=None) -> str | None
176
+ ```
177
+
178
+ ### `ReactLoop`
179
+
180
+ ```python
181
+ loop = ReactLoop()
182
+ await loop.runtime(*, task, system_prompt=None, history=None) -> str | None
183
+ ```
184
+
185
+ ### `StepOutcome`
186
+
187
+ ```python
188
+ @dataclass
189
+ class StepOutcome:
190
+ data: Any # tool return value
191
+ next_prompt: str | None # None = task complete
192
+ should_exit: bool # True = force exit
193
+ ```
194
+
195
+ ## Requirements
196
+
197
+ - Python >= 3.12
198
+ - fastmcp >= 3.4.2
199
+ - openai >= 2.41.0
200
+ - python-dotenv >= 1.2.2
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,16 @@
1
+ allagent/__init__.py,sha256=6PWlgh_UTf8CN73z-tWAEnMMgIxEUUkDgHv2gF7EYmQ,551
2
+ allagent/logger.py,sha256=c68Nfd9mx0di_O_03pD6TFphEXQkhcsP9l3WnGSBS2g,1231
3
+ allagent/agent/__init__.py,sha256=QsBZstW1NzZrxqFeieid64wTkJbfS5ddB4T0YFLhaos,236
4
+ allagent/agent/base.py,sha256=GFY-Y_OsWbhDB9sYMJJu3nTuIrKpTG9MClWEAH-fxNQ,6745
5
+ allagent/agent/tool_schema.py,sha256=8fOHDng34YbK40_sg_kgmcP2T9hzGJDuvJ0QIhT41dA,1032
6
+ allagent/agent/chat/chat.py,sha256=_HD4bcjyxptDT5FYRg0Qh3XhGGQK3aOGyRF8u_fIuXE,1406
7
+ allagent/agent/react/mcp_config.json,sha256=Kce3W0yVauJxpiFaVXXRvYAp0BAKOe6-4NrWG_rTIUg,160
8
+ allagent/agent/react/reactor.py,sha256=9FaLj2s7fowoXvncDAjwt4rMwv5Jrok2EAkHIufM9x4,4207
9
+ allagent/agent/react/react_skill/weather/SKILL.md,sha256=T1hYcIiXC4MA__s8y6cgDf6NxSTRdZBWOwUmFsyPI88,1854
10
+ allagent/plugins/__init__.py,sha256=LUEkGUtzKIyM9Ay4BxAx2APEJVyDtwOZtEgvX8Q5muA,237
11
+ allagent/plugins/mcp/mcp_manager.py,sha256=yyEWrdOkFvh4oZP6ene_TY9sVOwmkpQFnMBzLenbc6I,4691
12
+ allagent/plugins/skill/skill_manager.py,sha256=fnD51c4Jhp_c2UrJqka1EPKWDLH5QoakXNWIppTUy7E,8222
13
+ simagentplg-0.1.0.dist-info/METADATA,sha256=EkqqM-jothAVvCBMksW1ifRMMaVH9XgUV9enj3SVwDs,5674
14
+ simagentplg-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ simagentplg-0.1.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
16
+ simagentplg-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.