auto-agent-kit 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.
@@ -0,0 +1,239 @@
1
+ """MCPServer — MCP 协议服务器
2
+
3
+ JSON-RPC 2.0 + SSE 传输,用于 Agent 工具暴露和远程调用。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Callable, Optional
14
+
15
+ logger = logging.getLogger("auto_agent_kit.mcp")
16
+
17
+
18
+ @dataclass
19
+ class MCPTool:
20
+ """MCP 工具定义"""
21
+ name: str
22
+ description: str
23
+ handler: Callable
24
+ parameters: dict = field(default_factory=lambda: {"type": "object", "properties": {}})
25
+
26
+
27
+ @dataclass
28
+ class MCPRequest:
29
+ """JSON-RPC 请求"""
30
+ jsonrpc: str = "2.0"
31
+ method: str = ""
32
+ params: Any = None
33
+ id: Optional[str] = None
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict) -> "MCPRequest":
37
+ return cls(
38
+ jsonrpc=data.get("jsonrpc", "2.0"),
39
+ method=data.get("method", ""),
40
+ params=data.get("params"),
41
+ id=data.get("id"),
42
+ )
43
+
44
+ def to_response(self, result: Any = None, error: Optional[dict] = None) -> dict:
45
+ resp = {"jsonrpc": "2.0", "id": self.id}
46
+ if error:
47
+ resp["error"] = error
48
+ else:
49
+ resp["result"] = result
50
+ return resp
51
+
52
+
53
+ class MCPServer:
54
+ """MCP 协议服务器 — JSON-RPC 2.0 + SSE"""
55
+
56
+ def __init__(self, host: str = "0.0.0.0", port: int = 8901):
57
+ self.host = host
58
+ self.port = port
59
+ self._tools: dict[str, MCPTool] = {}
60
+ self._request_log: list[dict] = []
61
+ self._max_log: int = 500
62
+ self._app: Any = None # FastAPI app (lazy init)
63
+ self._started: bool = False
64
+
65
+ def register_tool(self, name: str, description: str, handler: Callable,
66
+ parameters: Optional[dict] = None):
67
+ """注册一个 MCP 工具"""
68
+ self._tools[name] = MCPTool(
69
+ name=name,
70
+ description=description,
71
+ handler=handler,
72
+ parameters=parameters or {"type": "object", "properties": {}},
73
+ )
74
+
75
+ def register_tools(self, tools: list[dict]):
76
+ """批量注册工具"""
77
+ for t in tools:
78
+ self.register_tool(
79
+ name=t["name"],
80
+ description=t.get("description", ""),
81
+ handler=t["handler"],
82
+ parameters=t.get("parameters"),
83
+ )
84
+
85
+ def handle_request(self, raw: dict) -> dict:
86
+ """处理 JSON-RPC 请求"""
87
+ try:
88
+ req = MCPRequest.from_dict(raw)
89
+ except Exception as e:
90
+ return {"jsonrpc": "2.0", "id": None, "error": {
91
+ "code": -32700, "message": f"Parse error: {e}"
92
+ }}
93
+
94
+ # 记录请求
95
+ self._request_log.append({
96
+ "timestamp": time.time(),
97
+ "method": req.method,
98
+ "id": req.id,
99
+ })
100
+ if len(self._request_log) > self._max_log:
101
+ self._request_log = self._request_log[-self._max_log:]
102
+
103
+ # 内置方法
104
+ if req.method == "list_tools":
105
+ return req.to_response(result={
106
+ "tools": [
107
+ {"name": t.name, "description": t.description, "parameters": t.parameters}
108
+ for t in self._tools.values()
109
+ ]
110
+ })
111
+
112
+ if req.method == "ping":
113
+ return req.to_response(result={"pong": True, "timestamp": time.time()})
114
+
115
+ # 工具调用
116
+ if req.method == "call_tool":
117
+ tool_name = req.params.get("name", "") if req.params else ""
118
+ tool_args = req.params.get("arguments", {}) if req.params else {}
119
+
120
+ tool = self._tools.get(tool_name)
121
+ if not tool:
122
+ return req.to_response(error={
123
+ "code": -32601,
124
+ "message": f"Tool not found: {tool_name}",
125
+ })
126
+
127
+ try:
128
+ result = tool.handler(**tool_args)
129
+ return req.to_response(result={
130
+ "content": [{"type": "text", "text": str(result)}],
131
+ })
132
+ except Exception as e:
133
+ logger.error(f"Tool {tool_name} failed: {e}")
134
+ return req.to_response(error={
135
+ "code": -32000,
136
+ "message": f"Tool execution error: {e}",
137
+ })
138
+
139
+ # 未知方法
140
+ return req.to_response(error={
141
+ "code": -32601,
142
+ "message": f"Method not found: {req.method}",
143
+ })
144
+
145
+ def build_app(self):
146
+ """构建 FastAPI 应用(可选依赖)"""
147
+ try:
148
+ from fastapi import FastAPI, Request
149
+ from fastapi.responses import JSONResponse, StreamingResponse
150
+ except ImportError:
151
+ raise ImportError("FastAPI is required for MCPServer HTTP mode. "
152
+ "Install: pip install auto-agent-kit[mcp]")
153
+
154
+ app = FastAPI(title="AutoAgentKit MCP Server", version="0.1.0")
155
+
156
+ @app.get("/health")
157
+ async def health():
158
+ return {"status": "ok", "tools": len(self._tools)}
159
+
160
+ @app.get("/tools")
161
+ async def list_tools():
162
+ return {
163
+ "tools": [
164
+ {"name": t.name, "description": t.description}
165
+ for t in self._tools.values()
166
+ ]
167
+ }
168
+
169
+ @app.post("/rpc")
170
+ async def rpc(request: Request):
171
+ body = await request.json()
172
+ result = self.handle_request(body)
173
+ return JSONResponse(result)
174
+
175
+ @app.get("/sse")
176
+ async def sse(request: Request):
177
+ """SSE 端点"""
178
+ async def event_generator():
179
+ yield f"data: {json.dumps({'type': 'connected', 'tools': len(self._tools)})}\n\n"
180
+ # 保持连接
181
+ while True:
182
+ await asyncio.sleep(30)
183
+ yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
184
+
185
+ return StreamingResponse(
186
+ event_generator(),
187
+ media_type="text/event-stream",
188
+ headers={
189
+ "Cache-Control": "no-cache",
190
+ "Connection": "keep-alive",
191
+ },
192
+ )
193
+
194
+ self._app = app
195
+ return app
196
+
197
+ def start(self, host: Optional[str] = None, port: Optional[int] = None):
198
+ """启动 MCP 服务器"""
199
+ import asyncio
200
+ import uvicorn
201
+
202
+ host = host or self.host
203
+ port = port or self.port
204
+
205
+ app = self.build_app()
206
+ self._started = True
207
+ logger.info(f"MCP Server starting on {host}:{port}")
208
+ uvicorn.run(app, host=host, port=port, log_level="info")
209
+
210
+ def start_async(self, host: Optional[str] = None, port: Optional[int] = None):
211
+ """异步启动(在已有事件循环中使用)"""
212
+ import asyncio
213
+ import uvicorn
214
+
215
+ host = host or self.host
216
+ port = port or self.port
217
+
218
+ app = self.build_app()
219
+ self._started = True
220
+
221
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
222
+ server = uvicorn.Server(config)
223
+ return server.serve()
224
+
225
+ def get_stats(self) -> dict:
226
+ """获取服务器统计"""
227
+ return {
228
+ "started": self._started,
229
+ "host": self.host,
230
+ "port": self.port,
231
+ "tools_registered": len(self._tools),
232
+ "requests_handled": len(self._request_log),
233
+ "tool_names": list(self._tools.keys()),
234
+ }
235
+
236
+ def stop(self):
237
+ """停止服务器"""
238
+ self._started = False
239
+ logger.info("MCP Server stopped")
@@ -0,0 +1,174 @@
1
+ """PlanMode — 计划执行模式
2
+
3
+ 将复杂任务分解为可执行步骤,支持 Plan/Act 分离。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any, Callable, Optional
13
+
14
+
15
+ class StepStatus(Enum):
16
+ PENDING = "pending"
17
+ IN_PROGRESS = "in_progress"
18
+ COMPLETED = "completed"
19
+ FAILED = "failed"
20
+ SKIPPED = "skipped"
21
+
22
+
23
+ @dataclass
24
+ class PlanStep:
25
+ """单个计划步骤"""
26
+ id: str
27
+ description: str
28
+ status: StepStatus = StepStatus.PENDING
29
+ dependencies: list[str] = field(default_factory=list)
30
+ result: Optional[Any] = None
31
+ error: Optional[str] = None
32
+ started_at: Optional[float] = None
33
+ completed_at: Optional[float] = None
34
+
35
+ @property
36
+ def duration(self) -> Optional[float]:
37
+ if self.started_at and self.completed_at:
38
+ return self.completed_at - self.started_at
39
+ return None
40
+
41
+
42
+ @dataclass
43
+ class ExecutionPlan:
44
+ """完整的执行计划"""
45
+ goal: str
46
+ steps: list[PlanStep] = field(default_factory=list)
47
+ created_at: float = field(default_factory=time.time)
48
+ context: dict[str, Any] = field(default_factory=dict)
49
+
50
+ def add_step(self, description: str, dependencies: Optional[list[str]] = None) -> PlanStep:
51
+ step_id = f"step_{len(self.steps) + 1}"
52
+ step = PlanStep(
53
+ id=step_id,
54
+ description=description,
55
+ dependencies=dependencies or [],
56
+ )
57
+ self.steps.append(step)
58
+ return step
59
+
60
+ def get_next_ready(self) -> Optional[PlanStep]:
61
+ """获取下一个可执行的步骤(依赖已完成的步骤)"""
62
+ completed_ids = {s.id for s in self.steps if s.status == StepStatus.COMPLETED}
63
+ for step in self.steps:
64
+ if step.status != StepStatus.PENDING:
65
+ continue
66
+ if all(dep in completed_ids for dep in step.dependencies):
67
+ return step
68
+ return None
69
+
70
+ @property
71
+ def progress(self) -> float:
72
+ if not self.steps:
73
+ return 0.0
74
+ done = sum(1 for s in self.steps if s.status in (StepStatus.COMPLETED, StepStatus.SKIPPED))
75
+ return done / len(self.steps)
76
+
77
+ @property
78
+ def summary(self) -> str:
79
+ total = len(self.steps)
80
+ done = sum(1 for s in self.steps if s.status == StepStatus.COMPLETED)
81
+ failed = sum(1 for s in self.steps if s.status == StepStatus.FAILED)
82
+ return f"[{done}/{total} 完成, {failed} 失败, {self.progress*100:.0f}%]"
83
+
84
+
85
+ class PlanMode:
86
+ """计划执行模式 — 将复杂任务分解为可执行步骤"""
87
+
88
+ def __init__(self, max_retries: int = 2):
89
+ self.max_retries = max_retries
90
+ self.current_plan: Optional[ExecutionPlan] = None
91
+ self._history: list[dict] = []
92
+
93
+ def create_plan(self, goal: str, steps: Optional[list[str]] = None) -> ExecutionPlan:
94
+ """创建执行计划"""
95
+ plan = ExecutionPlan(goal=goal)
96
+ if steps:
97
+ for desc in steps:
98
+ plan.add_step(desc)
99
+ self.current_plan = plan
100
+ self._history.append({"action": "create_plan", "goal": goal, "steps": len(steps or [])})
101
+ return plan
102
+
103
+ def start_step(self, step_id: str) -> Optional[PlanStep]:
104
+ """开始执行一个步骤"""
105
+ if not self.current_plan:
106
+ return None
107
+ for step in self.current_plan.steps:
108
+ if step.id == step_id and step.status == StepStatus.PENDING:
109
+ step.status = StepStatus.IN_PROGRESS
110
+ step.started_at = time.time()
111
+ return step
112
+ return None
113
+
114
+ def complete_step(self, step_id: str, result: Any = None) -> Optional[PlanStep]:
115
+ """完成一个步骤"""
116
+ if not self.current_plan:
117
+ return None
118
+ for step in self.current_plan.steps:
119
+ if step.id == step_id and step.status == StepStatus.IN_PROGRESS:
120
+ step.status = StepStatus.COMPLETED
121
+ step.completed_at = time.time()
122
+ step.result = result
123
+ return step
124
+ return None
125
+
126
+ def fail_step(self, step_id: str, error: str) -> Optional[PlanStep]:
127
+ """标记步骤失败"""
128
+ if not self.current_plan:
129
+ return None
130
+ for step in self.current_plan.steps:
131
+ if step.id == step_id and step.status == StepStatus.IN_PROGRESS:
132
+ step.status = StepStatus.FAILED
133
+ step.completed_at = time.time()
134
+ step.error = error
135
+ return step
136
+ return None
137
+
138
+ def execute_sequentially(self, executor: Callable[[str], Any]) -> list[dict]:
139
+ """顺序执行所有步骤(无依赖)"""
140
+ if not self.current_plan:
141
+ return []
142
+ results = []
143
+ for step in self.current_plan.steps:
144
+ self.start_step(step.id)
145
+ for attempt in range(self.max_retries + 1):
146
+ try:
147
+ result = executor(step.description)
148
+ self.complete_step(step.id, result)
149
+ results.append({"step": step.id, "status": "ok", "result": result})
150
+ break
151
+ except Exception as e:
152
+ if attempt < self.max_retries:
153
+ continue
154
+ self.fail_step(step.id, str(e))
155
+ results.append({"step": step.id, "status": "failed", "error": str(e)})
156
+ return results
157
+
158
+ def get_plan_status(self) -> dict:
159
+ """获取计划状态摘要"""
160
+ if not self.current_plan:
161
+ return {"status": "no_plan"}
162
+ return {
163
+ "goal": self.current_plan.goal,
164
+ "progress": self.current_plan.progress,
165
+ "summary": self.current_plan.summary,
166
+ "steps": [
167
+ {"id": s.id, "description": s.description, "status": s.status.value}
168
+ for s in self.current_plan.steps
169
+ ],
170
+ }
171
+
172
+ def reset(self):
173
+ """重置计划"""
174
+ self.current_plan = None
@@ -0,0 +1,130 @@
1
+ """ToolRouter — 语义工具路由器
2
+
3
+ 阶段性工具暴露,每阶段 ≤ 8 工具,防止上下文膨胀。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Any, Optional
11
+
12
+
13
+ class ToolPhase(Enum):
14
+ """工具阶段"""
15
+ INIT = "init" # 初始阶段 — 核心工具
16
+ EXPLORE = "explore" # 探索阶段 — 搜索/读取工具
17
+ EXECUTE = "execute" # 执行阶段 — 全部工具
18
+ REVIEW = "review" # 审查阶段 — 验证/检查工具
19
+
20
+
21
+ @dataclass
22
+ class ToolDef:
23
+ """工具定义"""
24
+ name: str
25
+ description: str
26
+ phase: ToolPhase = ToolPhase.INIT
27
+ usage_count: int = 0
28
+ last_used: Optional[float] = None
29
+ is_active: bool = True
30
+
31
+
32
+ class ToolRouter:
33
+ """语义工具路由器 — 阶段性暴露工具,防止上下文膨胀"""
34
+
35
+ PHASE_LIMITS = {
36
+ ToolPhase.INIT: 5,
37
+ ToolPhase.EXPLORE: 6,
38
+ ToolPhase.EXECUTE: 8,
39
+ ToolPhase.REVIEW: 5,
40
+ }
41
+
42
+ def __init__(self):
43
+ self._tools: dict[str, ToolDef] = {}
44
+ self._current_phase: ToolPhase = ToolPhase.INIT
45
+ self._phase_history: list[ToolPhase] = []
46
+
47
+ def register(self, name: str, description: str, phase: ToolPhase = ToolPhase.INIT) -> ToolDef:
48
+ """注册一个工具"""
49
+ tool = ToolDef(name=name, description=description, phase=phase)
50
+ self._tools[name] = tool
51
+ return tool
52
+
53
+ def register_batch(self, tools: list[dict]) -> list[ToolDef]:
54
+ """批量注册工具"""
55
+ results = []
56
+ for t in tools:
57
+ results.append(self.register(
58
+ name=t["name"],
59
+ description=t["description"],
60
+ phase=ToolPhase(t.get("phase", "init")),
61
+ ))
62
+ return results
63
+
64
+ def set_phase(self, phase: ToolPhase) -> list[ToolDef]:
65
+ """切换阶段,返回当前阶段可用工具"""
66
+ self._phase_history.append(self._current_phase)
67
+ self._current_phase = phase
68
+ return self.get_active_tools()
69
+
70
+ def get_active_tools(self) -> list[ToolDef]:
71
+ """获取当前阶段可用工具"""
72
+ active = [
73
+ t for t in self._tools.values()
74
+ if t.phase == self._current_phase and t.is_active
75
+ ]
76
+ limit = self.PHASE_LIMITS.get(self._current_phase, 8)
77
+ return active[:limit]
78
+
79
+ def get_all_tools(self) -> list[ToolDef]:
80
+ """获取所有注册工具"""
81
+ return list(self._tools.values())
82
+
83
+ def use_tool(self, name: str) -> Optional[ToolDef]:
84
+ """记录工具使用"""
85
+ import time
86
+ tool = self._tools.get(name)
87
+ if tool:
88
+ tool.usage_count += 1
89
+ tool.last_used = time.time()
90
+ return tool
91
+
92
+ def deactivate(self, name: str) -> bool:
93
+ """停用工具"""
94
+ tool = self._tools.get(name)
95
+ if tool:
96
+ tool.is_active = False
97
+ return True
98
+ return False
99
+
100
+ def activate(self, name: str) -> bool:
101
+ """启用工具"""
102
+ tool = self._tools.get(name)
103
+ if tool:
104
+ tool.is_active = True
105
+ return True
106
+ return False
107
+
108
+ def get_stats(self) -> dict:
109
+ """获取工具使用统计"""
110
+ return {
111
+ "total_tools": len(self._tools),
112
+ "current_phase": self._current_phase.value,
113
+ "active_tools": len(self.get_active_tools()),
114
+ "phase_history": [p.value for p in self._phase_history],
115
+ "usage": {
116
+ name: {"count": t.usage_count, "phase": t.phase.value}
117
+ for name, t in self._tools.items()
118
+ },
119
+ }
120
+
121
+ def find_dead_tools(self, threshold_days: float = 30) -> list[str]:
122
+ """查找长期未使用的工具"""
123
+ import time
124
+ now = time.time()
125
+ threshold_seconds = threshold_days * 86400
126
+ dead = []
127
+ for name, tool in self._tools.items():
128
+ if tool.last_used and (now - tool.last_used) > threshold_seconds:
129
+ dead.append(name)
130
+ return dead