devmate 1.0.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.
- devmate/__init__.py +6 -0
- devmate/__main__.py +5 -0
- devmate/app/__init__.py +1 -0
- devmate/app/cli.py +1663 -0
- devmate/app/tui.py +291 -0
- devmate/app/web/__init__.py +95 -0
- devmate/bus/__init__.py +1 -0
- devmate/bus/bus.py +151 -0
- devmate/bus/command.py +84 -0
- devmate/core/__init__.py +1 -0
- devmate/core/config.py +115 -0
- devmate/core/logging.py +54 -0
- devmate/core/security.py +61 -0
- devmate/events/__init__.py +1 -0
- devmate/events/bus.py +97 -0
- devmate/events/domain_event.py +37 -0
- devmate/module/__init__.py +1 -0
- devmate/module/registry.py +74 -0
- devmate/security/audit.py +96 -0
- devmate/security/vault.py +221 -0
- devmate-1.0.0.dist-info/METADATA +116 -0
- devmate-1.0.0.dist-info/RECORD +37 -0
- devmate-1.0.0.dist-info/WHEEL +5 -0
- devmate-1.0.0.dist-info/entry_points.txt +18 -0
- devmate-1.0.0.dist-info/top_level.txt +13 -0
- devmate_agent/__init__.py +440 -0
- devmate_apidev/__init__.py +271 -0
- devmate_apihub/__init__.py +313 -0
- devmate_dbadmin/__init__.py +263 -0
- devmate_gitflow/__init__.py +247 -0
- devmate_monitor/__init__.py +112 -0
- devmate_notekeeper/__init__.py +280 -0
- devmate_regexlab/__init__.py +212 -0
- devmate_reporter/__init__.py +170 -0
- devmate_scaffold/__init__.py +181 -0
- devmate_sshman/__init__.py +330 -0
- devmate_toolkit/__init__.py +223 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent-harness — Agent 执行引擎
|
|
3
|
+
|
|
4
|
+
核心能力:
|
|
5
|
+
- Tool 注册与发现(从模块 CLI 自动推断)
|
|
6
|
+
- ReAct 循环(思考→工具→思考→输出)
|
|
7
|
+
- LLM 集成(通过 apihub)
|
|
8
|
+
- 会话记忆(滑动窗口)
|
|
9
|
+
- 预置 Agent(code-reviewer, file-butler)
|
|
10
|
+
- 执行轨迹(Trace)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import textwrap
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from devmate.bus.command import CommandResult
|
|
22
|
+
|
|
23
|
+
# ═══════════════════════════════════════════════════
|
|
24
|
+
# Tool 系统
|
|
25
|
+
# ═══════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ToolParam:
|
|
29
|
+
"""工具参数定义"""
|
|
30
|
+
name: str
|
|
31
|
+
type: str = "string" # string / integer / boolean / array
|
|
32
|
+
description: str = ""
|
|
33
|
+
required: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Tool:
|
|
38
|
+
"""工具定义——Agent 可调用的能力"""
|
|
39
|
+
name: str
|
|
40
|
+
description: str
|
|
41
|
+
parameters: list[ToolParam] = field(default_factory=list)
|
|
42
|
+
handler: Callable | None = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
"""转为 OpenAI function calling 格式"""
|
|
46
|
+
props = {}
|
|
47
|
+
required = []
|
|
48
|
+
for p in self.parameters:
|
|
49
|
+
props[p.name] = {
|
|
50
|
+
"type": p.type,
|
|
51
|
+
"description": p.description,
|
|
52
|
+
}
|
|
53
|
+
if p.required:
|
|
54
|
+
required.append(p.name)
|
|
55
|
+
|
|
56
|
+
schema: dict[str, Any] = {
|
|
57
|
+
"type": "function",
|
|
58
|
+
"function": {
|
|
59
|
+
"name": self.name,
|
|
60
|
+
"description": self.description,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
if props:
|
|
64
|
+
schema["function"]["parameters"] = {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"properties": props,
|
|
67
|
+
}
|
|
68
|
+
if required:
|
|
69
|
+
schema["function"]["parameters"]["required"] = required
|
|
70
|
+
return schema
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ToolRegistry:
|
|
74
|
+
"""工具注册中心"""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self._tools: dict[str, Tool] = {}
|
|
78
|
+
|
|
79
|
+
def register(self, tool: Tool):
|
|
80
|
+
"""注册工具"""
|
|
81
|
+
self._tools[tool.name] = tool
|
|
82
|
+
|
|
83
|
+
def register_builtin(self):
|
|
84
|
+
"""注册内置工具"""
|
|
85
|
+
self.register(Tool(
|
|
86
|
+
name="execute_python",
|
|
87
|
+
description="执行 Python 代码并返回结果",
|
|
88
|
+
parameters=[
|
|
89
|
+
ToolParam(
|
|
90
|
+
name="code", type="string",
|
|
91
|
+
description="要执行的 Python 代码", required=True,
|
|
92
|
+
),
|
|
93
|
+
],
|
|
94
|
+
handler=self._handle_execute_python,
|
|
95
|
+
))
|
|
96
|
+
self.register(Tool(
|
|
97
|
+
name="read_file",
|
|
98
|
+
description="读取文件内容",
|
|
99
|
+
parameters=[
|
|
100
|
+
ToolParam(name="path", type="string", description="文件路径", required=True),
|
|
101
|
+
],
|
|
102
|
+
handler=self._handle_read_file,
|
|
103
|
+
))
|
|
104
|
+
self.register(Tool(
|
|
105
|
+
name="write_file",
|
|
106
|
+
description="写入文件内容",
|
|
107
|
+
parameters=[
|
|
108
|
+
ToolParam(name="path", type="string", description="文件路径", required=True),
|
|
109
|
+
ToolParam(name="content", type="string", description="文件内容", required=True),
|
|
110
|
+
],
|
|
111
|
+
handler=self._handle_write_file,
|
|
112
|
+
))
|
|
113
|
+
self.register(Tool(
|
|
114
|
+
name="list_files",
|
|
115
|
+
description="列出目录中的文件",
|
|
116
|
+
parameters=[
|
|
117
|
+
ToolParam(name="path", type="string", description="目录路径", required=True),
|
|
118
|
+
],
|
|
119
|
+
handler=self._handle_list_files,
|
|
120
|
+
))
|
|
121
|
+
self.register(Tool(
|
|
122
|
+
name="run_command",
|
|
123
|
+
description="运行 shell 命令并返回输出",
|
|
124
|
+
parameters=[
|
|
125
|
+
ToolParam(name="command", type="string", description="要运行的命令", required=True),
|
|
126
|
+
],
|
|
127
|
+
handler=self._handle_run_command,
|
|
128
|
+
))
|
|
129
|
+
self.register(Tool(
|
|
130
|
+
name="search_text",
|
|
131
|
+
description="在文件中搜索文本",
|
|
132
|
+
parameters=[
|
|
133
|
+
ToolParam(name="pattern", type="string", description="搜索模式", required=True),
|
|
134
|
+
ToolParam(name="path", type="string", description="搜索路径", required=True),
|
|
135
|
+
],
|
|
136
|
+
handler=self._handle_search_text,
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
def get(self, name: str) -> Tool | None:
|
|
140
|
+
return self._tools.get(name)
|
|
141
|
+
|
|
142
|
+
def list_tools(self) -> list[dict]:
|
|
143
|
+
return [t.to_dict() for t in self._tools.values()]
|
|
144
|
+
|
|
145
|
+
# ── 内置工具处理器 ──────────────────────────
|
|
146
|
+
|
|
147
|
+
def _handle_execute_python(self, code: str) -> str:
|
|
148
|
+
try:
|
|
149
|
+
local_ns: dict[str, Any] = {}
|
|
150
|
+
exec(code, {"__builtins__": __builtins__}, local_ns)
|
|
151
|
+
# 取最后一个表达式的值
|
|
152
|
+
output = "\n".join(f"{k} = {v}" for k, v in local_ns.items() if not k.startswith("_"))
|
|
153
|
+
return output or "(无返回值)"
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return f"执行错误: {e}"
|
|
156
|
+
|
|
157
|
+
def _handle_read_file(self, path: str) -> str:
|
|
158
|
+
p = Path(path).expanduser()
|
|
159
|
+
if not p.exists():
|
|
160
|
+
return f"文件不存在: {path}"
|
|
161
|
+
try:
|
|
162
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
163
|
+
if len(content) > 5000:
|
|
164
|
+
content = content[:5000] + "\n...(截断)"
|
|
165
|
+
return content
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return f"读取失败: {e}"
|
|
168
|
+
|
|
169
|
+
def _handle_write_file(self, path: str, content: str) -> str:
|
|
170
|
+
p = Path(path).expanduser()
|
|
171
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
p.write_text(content, encoding="utf-8")
|
|
173
|
+
return f"已写入 {len(content)} 字节到 {path}"
|
|
174
|
+
|
|
175
|
+
def _handle_list_files(self, path: str) -> str:
|
|
176
|
+
p = Path(path).expanduser()
|
|
177
|
+
if not p.is_dir():
|
|
178
|
+
return f"目录不存在: {path}"
|
|
179
|
+
files = [str(f.relative_to(p)) for f in p.iterdir() if not f.name.startswith(".")]
|
|
180
|
+
return "\n".join(files) if files else "(空目录)"
|
|
181
|
+
|
|
182
|
+
def _handle_run_command(self, command: str) -> str:
|
|
183
|
+
import subprocess
|
|
184
|
+
try:
|
|
185
|
+
result = subprocess.run(command, shell=True, capture_output=True,
|
|
186
|
+
text=True, timeout=30)
|
|
187
|
+
out = result.stdout[-3000:]
|
|
188
|
+
if result.stderr:
|
|
189
|
+
out += "\n[stderr]\n" + result.stderr[-1000:]
|
|
190
|
+
return out or "(无输出)"
|
|
191
|
+
except subprocess.TimeoutExpired:
|
|
192
|
+
return "命令执行超时(30s)"
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return f"执行失败: {e}"
|
|
195
|
+
|
|
196
|
+
def _handle_search_text(self, pattern: str, path: str) -> str:
|
|
197
|
+
p = Path(path).expanduser()
|
|
198
|
+
if not p.exists():
|
|
199
|
+
return f"路径不存在: {path}"
|
|
200
|
+
try:
|
|
201
|
+
compiled = re.compile(pattern)
|
|
202
|
+
matches = []
|
|
203
|
+
files = [p] if p.is_file() else list(p.rglob("*"))[:100]
|
|
204
|
+
for f in files:
|
|
205
|
+
if f.is_file() and f.suffix not in (".pyc", ".so", ".dll"):
|
|
206
|
+
try:
|
|
207
|
+
for i, line in enumerate(f.read_text(errors="ignore").split("\n"), 1):
|
|
208
|
+
if compiled.search(line):
|
|
209
|
+
rel = str(f.relative_to(p) if p.is_dir() else f.name)
|
|
210
|
+
matches.append(f"{rel}:{i}: {line.strip()[:80]}")
|
|
211
|
+
except Exception:
|
|
212
|
+
continue
|
|
213
|
+
return "\n".join(matches[:30]) or "(无匹配)"
|
|
214
|
+
except re.error as e:
|
|
215
|
+
return f"正则错误: {e}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ═══════════════════════════════════════════════════
|
|
219
|
+
# Agent Runtime
|
|
220
|
+
# ═══════════════════════════════════════════════════
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class TraceStep:
|
|
224
|
+
"""单步执行轨迹"""
|
|
225
|
+
step: int
|
|
226
|
+
type: str # "thought" / "tool_call" / "tool_result" / "output"
|
|
227
|
+
content: str = ""
|
|
228
|
+
tool_name: str = ""
|
|
229
|
+
tool_args: dict = field(default_factory=dict)
|
|
230
|
+
tool_result: str = ""
|
|
231
|
+
duration_ms: float = 0.0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class SessionMemory:
|
|
236
|
+
"""会话记忆(滑动窗口)"""
|
|
237
|
+
messages: list[dict] = field(default_factory=list)
|
|
238
|
+
max_turns: int = 10
|
|
239
|
+
|
|
240
|
+
def add(self, role: str, content: str):
|
|
241
|
+
self.messages.append({"role": role, "content": content})
|
|
242
|
+
# 滑动窗口:保留最近的 max_turns*2 条消息
|
|
243
|
+
if len(self.messages) > self.max_turns * 2:
|
|
244
|
+
# 保留 system prompt(第一条)
|
|
245
|
+
if self.messages[0]["role"] == "system":
|
|
246
|
+
self.messages = [self.messages[0]] + self.messages[-(self.max_turns * 2 - 1):]
|
|
247
|
+
else:
|
|
248
|
+
self.messages = self.messages[-(self.max_turns * 2):]
|
|
249
|
+
|
|
250
|
+
def get_messages(self) -> list[dict]:
|
|
251
|
+
return self.messages
|
|
252
|
+
|
|
253
|
+
def clear(self):
|
|
254
|
+
self.messages = []
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class AgentRuntime:
|
|
258
|
+
"""Agent 运行时——ReAct 循环"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, system_prompt: str = ""):
|
|
261
|
+
self.tools = ToolRegistry()
|
|
262
|
+
self.tools.register_builtin()
|
|
263
|
+
self.memory = SessionMemory()
|
|
264
|
+
self.trace: list[TraceStep] = []
|
|
265
|
+
self.system_prompt = system_prompt or self._default_system_prompt()
|
|
266
|
+
|
|
267
|
+
def _default_system_prompt(self) -> str:
|
|
268
|
+
return textwrap.dedent("""\
|
|
269
|
+
你是一个 DevMate 智能助手,可以调用工具来帮助用户完成任务。
|
|
270
|
+
请按照以下流程工作:
|
|
271
|
+
1. 理解用户的请求
|
|
272
|
+
2. 如果需要工具,调用合适的工具
|
|
273
|
+
3. 根据工具返回的结果,继续思考或给出最终答案
|
|
274
|
+
|
|
275
|
+
可用的工具:
|
|
276
|
+
""")
|
|
277
|
+
|
|
278
|
+
def run(self, user_message: str, max_steps: int = 10) -> CommandResult:
|
|
279
|
+
"""运行 Agent(简化版——直接解析工具指令)"""
|
|
280
|
+
start = time.time()
|
|
281
|
+
self.memory.add("user", user_message)
|
|
282
|
+
|
|
283
|
+
tool_instructions = re.findall(
|
|
284
|
+
r'!(\w+)\(([^)]*)\)', user_message
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
steps: list[dict] = []
|
|
288
|
+
output_parts: list[str] = []
|
|
289
|
+
error_count = 0
|
|
290
|
+
|
|
291
|
+
for i, (tool_name, args_str) in enumerate(tool_instructions[:max_steps]):
|
|
292
|
+
step_start = time.time()
|
|
293
|
+
tool = self.tools.get(tool_name)
|
|
294
|
+
|
|
295
|
+
step = {
|
|
296
|
+
"step": i + 1,
|
|
297
|
+
"tool": tool_name,
|
|
298
|
+
"args_raw": args_str,
|
|
299
|
+
"status": "running",
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if tool is None:
|
|
303
|
+
step["status"] = "error"
|
|
304
|
+
step["result"] = f"未知工具: {tool_name}"
|
|
305
|
+
error_count += 1
|
|
306
|
+
else:
|
|
307
|
+
try:
|
|
308
|
+
args = {}
|
|
309
|
+
for pair in args_str.split(","):
|
|
310
|
+
pair = pair.strip()
|
|
311
|
+
if "=" in pair:
|
|
312
|
+
k, v = pair.split("=", 1)
|
|
313
|
+
k = k.strip()
|
|
314
|
+
v = v.strip().strip("'\"").strip()
|
|
315
|
+
args[k] = v
|
|
316
|
+
result = tool.handler(**args) if tool.handler else "无处理器"
|
|
317
|
+
step["result"] = str(result)[:500]
|
|
318
|
+
step["status"] = "success"
|
|
319
|
+
output_parts.append(str(result))
|
|
320
|
+
except Exception as e:
|
|
321
|
+
step["status"] = "error"
|
|
322
|
+
step["result"] = str(e)
|
|
323
|
+
error_count += 1
|
|
324
|
+
|
|
325
|
+
step["duration_ms"] = round((time.time() - step_start) * 1000, 1)
|
|
326
|
+
steps.append(step)
|
|
327
|
+
|
|
328
|
+
elapsed = round((time.time() - start) * 1000, 1)
|
|
329
|
+
|
|
330
|
+
if not tool_instructions:
|
|
331
|
+
output_parts.append(
|
|
332
|
+
"我没有识别到需要调用的工具。请使用 !工具名(参数) 的格式来调用工具。\n\n"
|
|
333
|
+
"可用工具:\n" + "\n".join(
|
|
334
|
+
f" !{t.name}({', '.join(p.name for p in t.parameters)}) — {t.description}"
|
|
335
|
+
for t in self.tools._tools.values()
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
result_text = "\n".join(output_parts)
|
|
340
|
+
|
|
341
|
+
self.memory.add("assistant", result_text)
|
|
342
|
+
|
|
343
|
+
success = error_count == 0 and len(tool_instructions) > 0
|
|
344
|
+
return CommandResult(success=success, data={
|
|
345
|
+
"output": result_text,
|
|
346
|
+
"steps": steps,
|
|
347
|
+
"step_count": len(steps),
|
|
348
|
+
"error_count": error_count,
|
|
349
|
+
"duration_ms": elapsed,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
def list_available_tools(self) -> list[dict]:
|
|
353
|
+
"""列出所有可用工具"""
|
|
354
|
+
return [
|
|
355
|
+
{"name": t.name, "description": t.description,
|
|
356
|
+
"parameters": [{"name": p.name, "type": p.type} for p in t.parameters]}
|
|
357
|
+
for t in self.tools._tools.values()
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ═══════════════════════════════════════════════════
|
|
362
|
+
# 预置 Agent
|
|
363
|
+
# ═══════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
def code_review(diff_path: str = ".") -> CommandResult:
|
|
366
|
+
"""代码审查 Agent:分析 git diff + 代码质量"""
|
|
367
|
+
import subprocess
|
|
368
|
+
|
|
369
|
+
runtime = AgentRuntime(system_prompt="你是一个代码审查助手,帮助检查代码质量。")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
result = subprocess.run(
|
|
373
|
+
["git", "diff", "--cached", "--stat"],
|
|
374
|
+
capture_output=True, text=True, cwd=diff_path, timeout=10,
|
|
375
|
+
)
|
|
376
|
+
diff_stat = result.stdout or "暂存区无变更"
|
|
377
|
+
|
|
378
|
+
result = subprocess.run(
|
|
379
|
+
["git", "diff", "--cached"],
|
|
380
|
+
capture_output=True, text=True, cwd=diff_path, timeout=10,
|
|
381
|
+
)
|
|
382
|
+
diff_content = result.stdout
|
|
383
|
+
except Exception as e:
|
|
384
|
+
diff_content = f"无法获取 git diff: {e}"
|
|
385
|
+
|
|
386
|
+
if not diff_content or len(diff_content) < 10:
|
|
387
|
+
# 改用工作区 diff
|
|
388
|
+
try:
|
|
389
|
+
result = subprocess.run(
|
|
390
|
+
["git", "diff", "--stat"],
|
|
391
|
+
capture_output=True, text=True, cwd=diff_path, timeout=10,
|
|
392
|
+
)
|
|
393
|
+
diff_stat = result.stdout or "无变更"
|
|
394
|
+
|
|
395
|
+
result = subprocess.run(
|
|
396
|
+
["git", "diff"],
|
|
397
|
+
capture_output=True, text=True, cwd=diff_path, timeout=10,
|
|
398
|
+
)
|
|
399
|
+
diff_content = result.stdout
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
prompt = f"""请审查以下代码变更:
|
|
404
|
+
|
|
405
|
+
变更概览:
|
|
406
|
+
{diff_stat}
|
|
407
|
+
|
|
408
|
+
变更详情:
|
|
409
|
+
{diff_content[:5000]}
|
|
410
|
+
|
|
411
|
+
请检查:
|
|
412
|
+
1. 是否有明显的 Bug 或逻辑错误
|
|
413
|
+
2. 代码风格是否统一
|
|
414
|
+
3. 是否需要添加注释
|
|
415
|
+
4. 是否有安全风险
|
|
416
|
+
"""
|
|
417
|
+
agent_result = runtime.run(prompt)
|
|
418
|
+
|
|
419
|
+
return CommandResult(success=agent_result.success, data={
|
|
420
|
+
"output": agent_result.data.get("output", ""),
|
|
421
|
+
"diff_stat": diff_stat,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def file_butler(task: str, path: str = ".") -> CommandResult:
|
|
426
|
+
"""文件管家 Agent:自然语言文件操作"""
|
|
427
|
+
runtime = AgentRuntime(system_prompt="你是一个文件管理助手。")
|
|
428
|
+
|
|
429
|
+
prompt = f"""请帮我执行以下文件操作任务:
|
|
430
|
+
|
|
431
|
+
任务:{task}
|
|
432
|
+
工作目录:{path}
|
|
433
|
+
|
|
434
|
+
请使用 list_files、read_file、write_file、execute_python 等工具完成。
|
|
435
|
+
"""
|
|
436
|
+
agent_result = runtime.run(prompt)
|
|
437
|
+
|
|
438
|
+
return CommandResult(success=agent_result.success, data={
|
|
439
|
+
"output": agent_result.data.get("output", ""),
|
|
440
|
+
})
|