Coverage for agentos/core/middleware.py: 58%
201 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
1"""
2AgentOS v1.1.4 Agent Runtime Middleware Pipeline — 可组合的执行生命周期中间件。
4在 Agent 执行的每个阶段(pre-LLM / post-LLM / pre-tool / post-tool)
5插入策略检查、日志、脱敏、预算控制等拦截逻辑。
7灵感来自 Microsoft Agent Framework 1.0 的 Middleware Pipeline 和 CrewAI Runtime Hooks。
8"""
10from __future__ import annotations
12from abc import ABC, abstractmethod
13from dataclasses import dataclass, field
14from enum import StrEnum
15from typing import Any
18class MiddlewarePhase(StrEnum):
19 """中间件触发阶段。"""
21 PRE_LLM = "pre_llm" # LLM 调用前
22 POST_LLM = "post_llm" # LLM 调用后、输出解析前
23 PRE_TOOL = "pre_tool" # 工具调用前
24 POST_TOOL = "post_tool" # 工具调用后
25 ON_ERROR = "on_error" # 执行出错时
26 ON_START = "on_start" # Agent 启动时
27 ON_COMPLETE = "on_complete" # Agent 执行完成时
30@dataclass
31class MiddlewareContext:
32 """中间件执行上下文。"""
34 phase: MiddlewarePhase
35 agent_name: str = ""
36 run_id: str = ""
37 # LLM 阶段
38 prompt: str | None = None
39 model_name: str | None = None
40 llm_output: str | None = None
41 # Tool 阶段
42 tool_name: str | None = None
43 tool_args: dict | None = None
44 tool_result: Any = None
45 # Error
46 error: Exception | None = None
47 # 额外元数据
48 metadata: dict[str, Any] = field(default_factory=dict)
51@dataclass
52class MiddlewareDecision:
53 """中间件决策结果。"""
55 allow: bool = True
56 """是否允许继续执行。"""
58 reason: str = ""
59 """决策理由。"""
61 modified_context: MiddlewareContext | None = None
62 """修改后的上下文(如脱敏后的 prompt)。"""
64 action: str = "allow" # allow / warn / block / transform / escalate
65 """决策动作。"""
67 blocked_by: str = ""
68 """阻断方名称。"""
71class AgentMiddleware(ABC):
72 """Agent 运行时中间件基类。
74 每个中间件声明自己监听的阶段,通过 process() 返回决策。
75 返回 MiddlewareDecision(allow=False) 阻断执行链。
77 __call__ 提供便捷调用:mw(ctx) → process(ctx)。
78 """
80 name: str = "base_middleware"
82 @property
83 def phases(self) -> list[MiddlewarePhase]:
84 """返回此中间件监听的阶段列表。"""
85 return [MiddlewarePhase.PRE_LLM]
87 @abstractmethod
88 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
89 """处理中间件逻辑。返回决策。"""
90 ...
92 async def __call__(self, ctx: MiddlewareContext) -> MiddlewareDecision:
93 """便捷调用,等价于 process(ctx)。"""
94 return await self.process(ctx)
97# ── 内置中间件 ──────────────────────────────────────────────────────────────
100class PIIMaskingMiddleware(AgentMiddleware):
101 """PII脱敏中间件:在 pre-LLM 阶段对 prompt 脱敏。"""
103 name = "pii_masking"
105 @property
106 def phases(self) -> list[MiddlewarePhase]:
107 return [MiddlewarePhase.PRE_LLM]
109 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
110 if not ctx.prompt:
111 return MiddlewareDecision(allow=True)
112 from agentos.security.guard import PIIDetector
114 detector = PIIDetector(auto_redact=True)
115 sanitized, items = detector.redact(ctx.prompt)
116 count = len(items)
117 if count > 0:
118 new_ctx = MiddlewareContext(**{**ctx.__dict__})
119 new_ctx.prompt = sanitized
120 new_ctx.metadata["pii_count"] = count
121 return MiddlewareDecision(
122 allow=True,
123 action="transform",
124 reason=f"Masked {count} PII instances",
125 modified_context=new_ctx,
126 )
127 return MiddlewareDecision(allow=True)
130class BudgetGuardMiddleware(AgentMiddleware):
131 """预算守护中间件:pre-LLM 阶段检查预算。"""
133 name = "budget_guard"
135 def __init__(self, tracker=None, budget_limit: float = 0.0, warn_ratio: float = 0.8):
136 self.tracker = tracker
137 self.budget_limit = budget_limit
138 self.warn_ratio = warn_ratio
140 @property
141 def phases(self) -> list[MiddlewarePhase]:
142 return [MiddlewarePhase.PRE_LLM, MiddlewarePhase.PRE_TOOL]
144 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
145 if not self.tracker or self.budget_limit <= 0:
146 return MiddlewareDecision(allow=True)
147 spent = self.tracker.total_cost
148 ratio = spent / self.budget_limit
149 if ratio >= 1.0:
150 return MiddlewareDecision(
151 allow=False,
152 action="block",
153 reason=f"Budget exceeded: ${spent:.4f} / ${self.budget_limit:.2f}",
154 blocked_by=self.name,
155 )
156 if ratio >= self.warn_ratio:
157 return MiddlewareDecision(
158 allow=True,
159 action="warn",
160 reason=f"Budget warning: {ratio:.0%} used (${spent:.4f} / ${self.budget_limit:.2f})",
161 )
162 return MiddlewareDecision(allow=True)
165class ToolRiskGuardMiddleware(AgentMiddleware):
166 """工具风险守护中间件:pre-tool 阶段根据风险等级决定是否阻断。"""
168 name = "tool_risk_guard"
170 def __init__(self, max_auto_level: str = "medium"):
171 from agentos.tools.risk import ToolRiskLevel
173 self.max_auto_level = ToolRiskLevel(max_auto_level)
175 @property
176 def phases(self) -> list[MiddlewarePhase]:
177 return [MiddlewarePhase.PRE_TOOL]
179 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
180 if not ctx.tool_name:
181 return MiddlewareDecision(allow=True)
183 from agentos.tools.risk import infer_risk_level
185 risk = infer_risk_level(ctx.tool_name, tool_args=ctx.tool_args)
187 if risk.requires_user_confirm():
188 return MiddlewareDecision(
189 allow=False,
190 action="escalate",
191 reason=f"Tool '{ctx.tool_name}' requires user approval: {risk.description}",
192 blocked_by=self.name,
193 )
195 levels = ["low", "medium", "high", "critical"]
196 if levels.index(risk.level.value) > levels.index(self.max_auto_level.value):
197 return MiddlewareDecision(
198 allow=False,
199 action="block",
200 reason=f"Tool '{ctx.tool_name}' risk {risk.level.value} exceeds auto limit {self.max_auto_level.value}",
201 blocked_by=self.name,
202 )
204 return MiddlewareDecision(allow=True)
207class AuditLogMiddleware(AgentMiddleware):
208 """审计日志中间件:在所有阶段记录审计轨迹。"""
210 name = "audit_log"
212 @property
213 def phases(self) -> list[MiddlewarePhase]:
214 return [
215 MiddlewarePhase.ON_START,
216 MiddlewarePhase.PRE_LLM,
217 MiddlewarePhase.PRE_TOOL,
218 MiddlewarePhase.POST_TOOL,
219 MiddlewarePhase.ON_ERROR,
220 MiddlewarePhase.ON_COMPLETE,
221 ]
223 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
224 import logging
226 logger = logging.getLogger("agentos.audit")
227 logger.info(
228 f"[{ctx.phase.value}] agent={ctx.agent_name} run={ctx.run_id} "
229 f"tool={ctx.tool_name or '-'}"
230 )
231 return MiddlewareDecision(allow=True)
234class TimingMiddleware(AgentMiddleware):
235 """计时中间件:记录每个阶段的耗时。"""
237 name = "timing"
239 def __init__(self):
240 self.timings: dict[str, float] = {}
241 self._phase_start: dict[str, float] = {}
243 @property
244 def phases(self) -> list[MiddlewarePhase]:
245 return [
246 MiddlewarePhase.ON_START,
247 MiddlewarePhase.PRE_LLM,
248 MiddlewarePhase.POST_LLM,
249 MiddlewarePhase.PRE_TOOL,
250 MiddlewarePhase.POST_TOOL,
251 MiddlewarePhase.ON_COMPLETE,
252 MiddlewarePhase.ON_ERROR,
253 ]
255 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
256 import time
258 phase_key = f"{ctx.phase.value}.{ctx.tool_name or ctx.agent_name or 'default'}"
259 self._phase_start[phase_key] = time.monotonic()
260 self.timings[phase_key] = self.timings.get(phase_key, 0.0)
261 return MiddlewareDecision(allow=True)
263 def get_timings(self) -> dict[str, float]:
264 return dict(self.timings)
266 def total_ms(self) -> float:
267 return sum(self.timings.values()) * 1000
270class RetryMiddleware(AgentMiddleware):
271 """重试中间件:在 ON_ERROR 阶段自动重试。"""
273 name = "retry"
275 def __init__(self, max_retries: int = 2, backoff_base: float = 1.0):
276 self.max_retries = max_retries
277 self.backoff_base = backoff_base
278 self._retry_counts: dict[str, int] = {}
280 @property
281 def phases(self) -> list[MiddlewarePhase]:
282 return [MiddlewarePhase.ON_ERROR]
284 async def process(self, ctx: MiddlewareContext) -> MiddlewareDecision:
285 import asyncio
287 run_key = ctx.run_id or "default"
288 count = self._retry_counts.get(run_key, 0)
290 if count >= self.max_retries:
291 return MiddlewareDecision(
292 allow=True,
293 action="warn",
294 reason=f"Max retries ({self.max_retries}) exhausted",
295 )
297 self._retry_counts[run_key] = count + 1
298 delay = self.backoff_base * (2**count)
299 await asyncio.sleep(delay)
301 return MiddlewareDecision(
302 allow=True,
303 action="warn",
304 reason=f"Retry {count + 1}/{self.max_retries} after {delay:.1f}s",
305 )
307 def reset(self, run_id: str = "") -> None:
308 if run_id:
309 self._retry_counts.pop(run_id, None)
310 else:
311 self._retry_counts.clear()
314# ── 中间件管道 ──────────────────────────────────────────────────────────────
317class MiddlewarePipeline:
318 """编排多个中间件按阶段执行。
320 每个阶段:
321 1. 筛选监听该阶段的中间件
322 2. 按注册顺序依次执行
323 3. 任一返回 allow=False 即阻断
324 4. 若返回 modified_context 则传递给后续中间件
325 """
327 def __init__(self, middlewares: list[AgentMiddleware] | None = None):
328 self._middlewares: list[AgentMiddleware] = list(middlewares or [])
330 def add(self, middleware: AgentMiddleware) -> MiddlewarePipeline:
331 """添加中间件,返回自身以支持链式调用。"""
332 self._middlewares.append(middleware)
333 return self
335 def remove(self, name: str) -> None:
336 self._middlewares = [m for m in self._middlewares if m.name != name]
338 @property
339 def middleware_names(self) -> list[str]:
340 return [m.name for m in self._middlewares]
342 async def execute_phase(
343 self,
344 phase: MiddlewarePhase,
345 ctx: MiddlewareContext,
346 ) -> MiddlewareDecision:
347 """执行指定阶段的所有中间件。"""
348 current_ctx = ctx
349 for mw in self._middlewares:
350 if phase not in mw.phases:
351 continue
352 decision = await mw.process(current_ctx)
353 if not decision.allow:
354 decision.blocked_by = mw.name
355 return decision
356 if decision.modified_context:
357 current_ctx = decision.modified_context
358 return MiddlewareDecision(allow=True, modified_context=current_ctx)
360 async def on_start(self, ctx: MiddlewareContext) -> MiddlewareDecision:
361 return await self.execute_phase(MiddlewarePhase.ON_START, ctx)
363 async def pre_llm(self, ctx: MiddlewareContext) -> MiddlewareDecision:
364 return await self.execute_phase(MiddlewarePhase.PRE_LLM, ctx)
366 async def post_llm(self, ctx: MiddlewareContext) -> MiddlewareDecision:
367 return await self.execute_phase(MiddlewarePhase.POST_LLM, ctx)
369 async def pre_tool(self, ctx: MiddlewareContext) -> MiddlewareDecision:
370 return await self.execute_phase(MiddlewarePhase.PRE_TOOL, ctx)
372 async def post_tool(self, ctx: MiddlewareContext) -> MiddlewareDecision:
373 return await self.execute_phase(MiddlewarePhase.POST_TOOL, ctx)
375 async def on_error(self, ctx: MiddlewareContext) -> MiddlewareDecision:
376 return await self.execute_phase(MiddlewarePhase.ON_ERROR, ctx)
378 async def on_complete(self, ctx: MiddlewareContext) -> MiddlewareDecision:
379 return await self.execute_phase(MiddlewarePhase.ON_COMPLETE, ctx)
381 async def run(
382 self,
383 ctx: MiddlewareContext,
384 phases: list[MiddlewarePhase] | None = None,
385 ) -> MiddlewareDecision:
386 """便捷方法:按阶段列表依次执行管道。
388 phases 默认为完整的生命周期序列。
389 """
390 if phases is None:
391 phases = [
392 MiddlewarePhase.ON_START,
393 MiddlewarePhase.PRE_LLM,
394 MiddlewarePhase.POST_LLM,
395 MiddlewarePhase.ON_COMPLETE,
396 ]
397 decision = MiddlewareDecision(allow=True, modified_context=ctx)
398 for phase in phases:
399 current_ctx = decision.modified_context or ctx
400 decision = await self.execute_phase(phase, current_ctx)
401 if not decision.allow:
402 return decision
403 return decision