SimAgentPlg 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.
- simagentplg-0.1.0/.env_example +8 -0
- simagentplg-0.1.0/.gitignore +13 -0
- simagentplg-0.1.0/.python-version +1 -0
- simagentplg-0.1.0/LICENSE +21 -0
- simagentplg-0.1.0/PKG-INFO +204 -0
- simagentplg-0.1.0/README.md +172 -0
- simagentplg-0.1.0/pyproject.toml +19 -0
- simagentplg-0.1.0/src/allagent/__init__.py +17 -0
- simagentplg-0.1.0/src/allagent/agent/__init__.py +7 -0
- simagentplg-0.1.0/src/allagent/agent/base.py +223 -0
- simagentplg-0.1.0/src/allagent/agent/chat/chat.py +56 -0
- simagentplg-0.1.0/src/allagent/agent/react/mcp_config.json +10 -0
- simagentplg-0.1.0/src/allagent/agent/react/react_skill/weather/SKILL.md +43 -0
- simagentplg-0.1.0/src/allagent/agent/react/reactor.py +123 -0
- simagentplg-0.1.0/src/allagent/agent/tool_schema.py +29 -0
- simagentplg-0.1.0/src/allagent/logger.py +46 -0
- simagentplg-0.1.0/src/allagent/plugins/__init__.py +6 -0
- simagentplg-0.1.0/src/allagent/plugins/mcp/mcp_manager.py +118 -0
- simagentplg-0.1.0/src/allagent/plugins/skill/skill_manager.py +246 -0
- simagentplg-0.1.0/tests/__init__.py +0 -0
- simagentplg-0.1.0/tests/mcp_test.py +13 -0
- simagentplg-0.1.0/tests/react_test.py +14 -0
- simagentplg-0.1.0/tests/skill_test.py +28 -0
- simagentplg-0.1.0/uv.lock +1419 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -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.
|
|
@@ -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,172 @@
|
|
|
1
|
+
# All Agent
|
|
2
|
+
|
|
3
|
+
A lightweight multi-agent framework with ReAct reasoning, tool dispatch, and MCP integration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **ReAct Agent** — ReAct (Reasoning + Acting) loop with multi-turn tool calling
|
|
8
|
+
- **Chat Agent** — simple conversational agent with multi-turn history support
|
|
9
|
+
- **Tool Dispatch** — convention-over-configuration: define `do_{tool_name}` methods, auto-routed via reflection
|
|
10
|
+
- **MCP Integration** — pluggable MCP server manager for external tool providers
|
|
11
|
+
- **Skill System** — skill-based prompt injection for domain-specific behaviors
|
|
12
|
+
- **Built-in Bash Executor** — async sandboxed bash execution with timeout, output truncation, and blacklist filtering
|
|
13
|
+
- **Stateless Execution** — each `runtime()` call starts with a clean context; history is caller-managed
|
|
14
|
+
- **OpenAI-compatible** — works with any OpenAI-compatible API (DeepSeek, etc.)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install all-agent
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with uv:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install all-agent
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Set up your environment variables (`.env`):
|
|
31
|
+
|
|
32
|
+
```env
|
|
33
|
+
CHAT_MODEL=deepseek-chat
|
|
34
|
+
MODEL_API_KEY=sk-xxxxxxxx
|
|
35
|
+
MODEL_URL=https://api.deepseek.com
|
|
36
|
+
LLM_TIMEOUT=30
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Chat Agent
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from allagent import ChatLoop
|
|
43
|
+
|
|
44
|
+
loop = ChatLoop()
|
|
45
|
+
result = await loop.runtime(task="介绍一下你自己")
|
|
46
|
+
|
|
47
|
+
# With multi-turn history
|
|
48
|
+
history = [
|
|
49
|
+
{"role": "user", "content": "今天天气不错"},
|
|
50
|
+
{"role": "assistant", "content": "是啊,适合出去走走"},
|
|
51
|
+
]
|
|
52
|
+
result = await loop.runtime(task="我们去哪", history=history)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### ReAct Agent
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from allagent import ReactLoop
|
|
59
|
+
|
|
60
|
+
loop = ReactLoop()
|
|
61
|
+
result = await loop.runtime(task="帮我写一个Python脚本打印当前时间")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The ReAct agent supports built-in tools (like `bash_run`) and any MCP tools configured in `mcp_config.json`.
|
|
65
|
+
|
|
66
|
+
### MCP Configuration
|
|
67
|
+
|
|
68
|
+
Place an `mcp_config.json` alongside your ReactLoop:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"playwright": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "@anthropic/mcp-playwright"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
LLMConfig (BaseHandler, ABC)
|
|
85
|
+
├── ChatLoop — stateless conversational agent
|
|
86
|
+
├── ReactLoop — ReAct reasoning + tool dispatch
|
|
87
|
+
│ ├── MCP tools — external tools via MCP protocol
|
|
88
|
+
│ ├── Skill system — domain-specific prompt injection
|
|
89
|
+
│ └── Local tools — built-in bash_run, extensible
|
|
90
|
+
└── (future) PlanLoop / ExecuteLoop
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Tool Dispatch Flow
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
LLM calls "bash_run"
|
|
97
|
+
→ BaseHandler.dispatch("bash_run", args)
|
|
98
|
+
→ hasattr(self, "do_bash_run")? YES
|
|
99
|
+
→ await self.do_bash_run(args) ← local tool
|
|
100
|
+
→ NO
|
|
101
|
+
→ "未知工具" → MCP fallback ← external tool
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Adding a Local Tool
|
|
105
|
+
|
|
106
|
+
1. Define the tool schema in `tool_schema.py`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
LOCAL_TOOLS = [
|
|
110
|
+
{
|
|
111
|
+
"type": "function",
|
|
112
|
+
"function": {
|
|
113
|
+
"name": "calculator",
|
|
114
|
+
"description": "Evaluate a math expression",
|
|
115
|
+
"parameters": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"expression": {"type": "string", "description": "Math expression"}
|
|
119
|
+
},
|
|
120
|
+
"required": ["expression"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
2. Add the `do_calculator` method in `LLMConfig`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
async def do_calculator(self, args: dict) -> StepOutcome:
|
|
131
|
+
result = eval(args["expression"])
|
|
132
|
+
return StepOutcome(data=result, next_prompt="\n")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
All agents automatically inherit the new tool.
|
|
136
|
+
|
|
137
|
+
## API
|
|
138
|
+
|
|
139
|
+
### `ChatLoop`
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
loop = ChatLoop(temperature=0.7)
|
|
143
|
+
await loop.runtime(*, task, system_prompt=None, history=None) -> str | None
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `ReactLoop`
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
loop = ReactLoop()
|
|
150
|
+
await loop.runtime(*, task, system_prompt=None, history=None) -> str | None
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### `StepOutcome`
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
@dataclass
|
|
157
|
+
class StepOutcome:
|
|
158
|
+
data: Any # tool return value
|
|
159
|
+
next_prompt: str | None # None = task complete
|
|
160
|
+
should_exit: bool # True = force exit
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Requirements
|
|
164
|
+
|
|
165
|
+
- Python >= 3.12
|
|
166
|
+
- fastmcp >= 3.4.2
|
|
167
|
+
- openai >= 2.41.0
|
|
168
|
+
- python-dotenv >= 1.2.2
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "SimAgentPlg"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lightweight multi-agent framework with ReAct reasoning, tool dispatch, and MCP integration"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {file = "LICENSE"}
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"fastmcp>=3.4.2",
|
|
10
|
+
"openai>=2.41.0",
|
|
11
|
+
"python-dotenv>=1.2.2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/allagent"]
|
|
@@ -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,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")
|