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 +17 -0
- allagent/agent/__init__.py +7 -0
- allagent/agent/base.py +223 -0
- allagent/agent/chat/chat.py +56 -0
- allagent/agent/react/mcp_config.json +10 -0
- allagent/agent/react/react_skill/weather/SKILL.md +43 -0
- allagent/agent/react/reactor.py +123 -0
- allagent/agent/tool_schema.py +29 -0
- allagent/logger.py +46 -0
- allagent/plugins/__init__.py +6 -0
- allagent/plugins/mcp/mcp_manager.py +118 -0
- allagent/plugins/skill/skill_manager.py +246 -0
- simagentplg-0.1.0.dist-info/METADATA +204 -0
- simagentplg-0.1.0.dist-info/RECORD +16 -0
- simagentplg-0.1.0.dist-info/WHEEL +4 -0
- simagentplg-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|
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,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,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,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.
|