feat(agents): Phase 6 tool system refactoring
Phase 6.1: ToolRegistry infrastructure - Add ToolManifest with ToolCategory, PermissionClass, SideEffectScope - Add ToolRegistry singleton with register/get/unregister/list/search - Add BaseTool abstract class with ReadTool/WriteTool/DBWriteTool/ExternalTool/NetworkTool subclasses - Add migration layer for backward compatibility Phase 6.2: Hook interception system - Add HookType (PRE_TOOL_USE, POST_TOOL_USE, TOOL_ERROR, TOOL_SKIP) - Add HookManager with singleton for hook registration - Add HookExecutor for pre/post/error hook execution Phase 6.3: Streaming execution - Add StreamingToolExecutor with batch execution support Phase 6.4: New builtin tools - Add file_tools: GlobTool, GrepTool, ReadFileTool, WriteFileTool - Add system_tools: BashTool, PowerShellTool - Add dev_tools: LSPTools, GitTool - Add collaboration_tools: TeamAgentTool, TaskBroadcastTool Tests: 29 passed
This commit is contained in:
46
backend/app/agents/tools/hooks/__init__.py
Normal file
46
backend/app/agents/tools/hooks/__init__.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Hook 系统 - Phase 6.2"""
|
||||
|
||||
from app.agents.tools.hooks.types import (
|
||||
HookDefinition,
|
||||
HookResult,
|
||||
HookStage,
|
||||
HookTrigger,
|
||||
HookType,
|
||||
ExecutionContext,
|
||||
HookHandler,
|
||||
PreToolHook,
|
||||
PostToolHook,
|
||||
ErrorToolHook,
|
||||
SkipToolHook,
|
||||
)
|
||||
from app.agents.tools.hooks.manager import (
|
||||
HookManager,
|
||||
get_hook_manager,
|
||||
reset_hook_manager,
|
||||
)
|
||||
from app.agents.tools.hooks.executor import (
|
||||
HookExecutor,
|
||||
get_hook_executor,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
"HookType",
|
||||
"HookStage",
|
||||
"HookTrigger",
|
||||
"HookDefinition",
|
||||
"HookResult",
|
||||
"ExecutionContext",
|
||||
"HookHandler",
|
||||
"PreToolHook",
|
||||
"PostToolHook",
|
||||
"ErrorToolHook",
|
||||
"SkipToolHook",
|
||||
# Manager
|
||||
"HookManager",
|
||||
"get_hook_manager",
|
||||
"reset_hook_manager",
|
||||
# Executor
|
||||
"HookExecutor",
|
||||
"get_hook_executor",
|
||||
]
|
||||
170
backend/app/agents/tools/hooks/executor.py
Normal file
170
backend/app/agents/tools/hooks/executor.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Hook 执行器 - Phase 6.2
|
||||
|
||||
执行 Hook 拦截逻辑。
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.manager import get_hook_manager
|
||||
from app.agents.tools.hooks.types import (
|
||||
HookDefinition,
|
||||
HookResult,
|
||||
HookType,
|
||||
ExecutionContext,
|
||||
)
|
||||
|
||||
|
||||
class HookExecutor:
|
||||
"""Hook 执行器
|
||||
|
||||
负责在工具执行前后执行 Hook 逻辑。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._manager = get_hook_manager()
|
||||
|
||||
async def execute_pre_hooks(
|
||||
self, context: ExecutionContext
|
||||
) -> tuple[bool, dict[str, Any] | None]:
|
||||
"""执行 pre-tool Hook
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
(是否继续执行, 修改后的输入)
|
||||
"""
|
||||
hooks = self._manager.get_hooks(HookType.PRE_TOOL_USE, context.tool_name)
|
||||
modified_input = context.tool_input
|
||||
|
||||
for hook in hooks:
|
||||
try:
|
||||
# 调用 hook handler
|
||||
handler = hook.handler
|
||||
if callable(handler):
|
||||
result = await self._call_hook(handler, context)
|
||||
if result and not result.continue_execution:
|
||||
# Hook 决定中断执行
|
||||
return False, modified_input
|
||||
if result.modified_input is not None:
|
||||
modified_input = result.modified_input
|
||||
except Exception as e:
|
||||
# Hook 出错,默认继续执行
|
||||
pass
|
||||
|
||||
return True, modified_input
|
||||
|
||||
async def execute_post_hooks(self, context: ExecutionContext, result: Any) -> Any:
|
||||
"""执行 post-tool Hook
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
result: 工具执行结果
|
||||
|
||||
Returns:
|
||||
修改后的结果
|
||||
"""
|
||||
hooks = self._manager.get_hooks(HookType.POST_TOOL_USE, context.tool_name)
|
||||
modified_result = result
|
||||
|
||||
for hook in hooks:
|
||||
try:
|
||||
handler = hook.handler
|
||||
if callable(handler):
|
||||
hook_result = await self._call_hook(handler, context, modified_result)
|
||||
if hook_result and hook_result.modified_output is not None:
|
||||
modified_result = hook_result.modified_output
|
||||
except Exception:
|
||||
# Hook 出错,默认保留原结果
|
||||
pass
|
||||
|
||||
return modified_result
|
||||
|
||||
async def execute_error_hooks(
|
||||
self, context: ExecutionContext, error: Exception
|
||||
) -> HookResult | None:
|
||||
"""执行 error Hook
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
error: 异常
|
||||
|
||||
Returns:
|
||||
Hook 结果,如果返回 None 则继续传播错误
|
||||
"""
|
||||
hooks = self._manager.get_hooks(HookType.TOOL_ERROR, context.tool_name)
|
||||
|
||||
for hook in hooks:
|
||||
try:
|
||||
handler = hook.handler
|
||||
if callable(handler):
|
||||
result = await self._call_hook(handler, context, error)
|
||||
if result is not None and result.continue_execution:
|
||||
return result
|
||||
except Exception:
|
||||
# Hook 出错,继续执行其他 error hooks
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
async def execute_skip_check(self, context: ExecutionContext) -> bool:
|
||||
"""检查是否应跳过工具执行
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
True 表示跳过,False 表示执行
|
||||
"""
|
||||
hooks = self._manager.get_hooks(HookType.TOOL_SKIP, context.tool_name)
|
||||
|
||||
for hook in hooks:
|
||||
try:
|
||||
handler = hook.handler
|
||||
if callable(handler):
|
||||
result = await self._call_hook(handler, context)
|
||||
if result is not None and isinstance(result, bool):
|
||||
return result
|
||||
except Exception:
|
||||
# Hook 出错,默认不跳过
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
async def _call_hook(
|
||||
self, handler: Any, context: ExecutionContext, *args: Any
|
||||
) -> HookResult | None:
|
||||
"""调用 Hook 处理函数
|
||||
|
||||
Args:
|
||||
handler: Hook 处理函数
|
||||
context: 执行上下文
|
||||
*args: 额外参数
|
||||
|
||||
Returns:
|
||||
Hook 结果
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# 如果是普通函数,直接调用
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
return await handler(context, *args)
|
||||
else:
|
||||
return handler(context, *args)
|
||||
|
||||
|
||||
# 全局单例
|
||||
_executor: HookExecutor | None = None
|
||||
|
||||
|
||||
def get_hook_executor() -> HookExecutor:
|
||||
"""获取全局 Hook 执行器
|
||||
|
||||
Returns:
|
||||
全局 HookExecutor 实例
|
||||
"""
|
||||
global _executor
|
||||
if _executor is None:
|
||||
_executor = HookExecutor()
|
||||
return _executor
|
||||
174
backend/app/agents/tools/hooks/manager.py
Normal file
174
backend/app/agents/tools/hooks/manager.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""Hook 管理器 - Phase 6.2
|
||||
|
||||
管理 Hook 的注册、查找和配置。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.types import (
|
||||
HookDefinition,
|
||||
HookResult,
|
||||
HookTrigger,
|
||||
HookType,
|
||||
ExecutionContext,
|
||||
)
|
||||
|
||||
|
||||
class HookManager:
|
||||
"""Hook 管理器
|
||||
|
||||
管理全局 Hook 的注册和配置。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._hooks: dict[HookType, list[HookDefinition]] = {
|
||||
HookType.PRE_TOOL_USE: [],
|
||||
HookType.POST_TOOL_USE: [],
|
||||
HookType.TOOL_ERROR: [],
|
||||
HookType.TOOL_SKIP: [],
|
||||
}
|
||||
self._global_hooks: list[HookDefinition] = [] # 全局 Hook(对所有工具生效)
|
||||
|
||||
def register(self, definition: HookDefinition) -> None:
|
||||
"""注册 Hook
|
||||
|
||||
Args:
|
||||
definition: Hook 定义
|
||||
"""
|
||||
if definition.trigger.tool_names is None and definition.trigger.categories is None:
|
||||
# 全局 Hook
|
||||
self._global_hooks.append(definition)
|
||||
else:
|
||||
# 特定工具 Hook
|
||||
self._hooks[definition.hook_type].append(definition)
|
||||
|
||||
# 按优先级排序
|
||||
self._hooks[definition.hook_type].sort(key=lambda h: h.priority, reverse=True)
|
||||
self._global_hooks.sort(key=lambda h: h.priority, reverse=True)
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""注销 Hook
|
||||
|
||||
Args:
|
||||
name: Hook 名称
|
||||
|
||||
Returns:
|
||||
是否成功注销
|
||||
"""
|
||||
# 从特定工具 Hook 中移除
|
||||
for hooks in self._hooks.values():
|
||||
for i, hook in enumerate(hooks):
|
||||
if hook.name == name:
|
||||
hooks.pop(i)
|
||||
return True
|
||||
|
||||
# 从全局 Hook 中移除
|
||||
for i, hook in enumerate(self._global_hooks):
|
||||
if hook.name == name:
|
||||
self._global_hooks.pop(i)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_hooks(self, hook_type: HookType, tool_name: str | None = None) -> list[HookDefinition]:
|
||||
"""获取指定类型和工具的 Hook
|
||||
|
||||
Args:
|
||||
hook_type: Hook 类型
|
||||
tool_name: 工具名称(可选)
|
||||
|
||||
Returns:
|
||||
匹配的 Hook 列表
|
||||
"""
|
||||
result: list[HookDefinition] = []
|
||||
|
||||
# 添加全局 Hook
|
||||
for hook in self._global_hooks:
|
||||
if hook.hook_type == hook_type and hook.enabled:
|
||||
result.append(hook)
|
||||
|
||||
# 添加特定工具 Hook
|
||||
for hook in self._hooks[hook_type]:
|
||||
if not hook.enabled:
|
||||
continue
|
||||
|
||||
if hook.trigger.tool_names is None and hook.trigger.categories is None:
|
||||
continue
|
||||
|
||||
# 检查是否匹配
|
||||
if hook.trigger.tool_names and tool_name not in hook.trigger.tool_names:
|
||||
continue
|
||||
|
||||
result.append(hook)
|
||||
|
||||
return result
|
||||
|
||||
def list_all(self) -> list[HookDefinition]:
|
||||
"""列出所有已注册的 Hook
|
||||
|
||||
Returns:
|
||||
Hook 列表
|
||||
"""
|
||||
all_hooks = list(self._global_hooks)
|
||||
for hooks in self._hooks.values():
|
||||
all_hooks.extend(hooks)
|
||||
return all_hooks
|
||||
|
||||
def enable(self, name: str) -> bool:
|
||||
"""启用 Hook
|
||||
|
||||
Args:
|
||||
name: Hook 名称
|
||||
|
||||
Returns:
|
||||
是否成功启用
|
||||
"""
|
||||
for hook in self.list_all():
|
||||
if hook.name == name:
|
||||
hook.enabled = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def disable(self, name: str) -> bool:
|
||||
"""禁用 Hook
|
||||
|
||||
Args:
|
||||
name: Hook 名称
|
||||
|
||||
Returns:
|
||||
是否成功禁用
|
||||
"""
|
||||
for hook in self.list_all():
|
||||
if hook.name == name:
|
||||
hook.enabled = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> None:
|
||||
"""清除所有 Hook"""
|
||||
self._hooks = {ht: [] for ht in HookType}
|
||||
self._global_hooks = []
|
||||
|
||||
|
||||
# 全局单例
|
||||
_global_hook_manager: HookManager | None = None
|
||||
|
||||
|
||||
def get_hook_manager() -> HookManager:
|
||||
"""获取全局 Hook 管理器
|
||||
|
||||
Returns:
|
||||
全局 HookManager 实例
|
||||
"""
|
||||
global _global_hook_manager
|
||||
if _global_hook_manager is None:
|
||||
_global_hook_manager = HookManager()
|
||||
return _global_hook_manager
|
||||
|
||||
|
||||
def reset_hook_manager() -> None:
|
||||
"""重置全局 Hook 管理器(用于测试)"""
|
||||
global _global_hook_manager
|
||||
if _global_hook_manager is not None:
|
||||
_global_hook_manager.clear()
|
||||
_global_hook_manager = None
|
||||
90
backend/app/agents/tools/hooks/types.py
Normal file
90
backend/app/agents/tools/hooks/types.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Hook 类型定义 - Phase 6.2
|
||||
|
||||
Hook 拦截系统类型定义。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class HookType(Enum):
|
||||
"""Hook 类型"""
|
||||
|
||||
PRE_TOOL_USE = "pre_tool_use" # 工具执行前
|
||||
POST_TOOL_USE = "post_tool_use" # 工具执行后
|
||||
TOOL_ERROR = "tool_error" # 工具执行出错
|
||||
TOOL_SKIP = "tool_skip" # 工具跳过(条件执行)
|
||||
|
||||
|
||||
class HookStage(Enum):
|
||||
"""Hook 执行阶段"""
|
||||
|
||||
BEFORE = "before"
|
||||
AFTER = "after"
|
||||
ON_ERROR = "on_error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookTrigger:
|
||||
"""Hook 触发条件"""
|
||||
|
||||
tool_names: list[str] | None = None # 只对特定工具生效,None 表示全部
|
||||
categories: list[str] | None = None # 只对特定类别生效
|
||||
conditions: dict[str, Any] | None = None # 自定义条件
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookDefinition:
|
||||
"""Hook 定义"""
|
||||
|
||||
name: str
|
||||
hook_type: HookType
|
||||
trigger: HookTrigger
|
||||
handler: Callable[..., Any] # Hook 处理函数
|
||||
priority: int = 0 # 优先级,数字越大越先执行
|
||||
enabled: bool = True
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookResult:
|
||||
"""Hook 执行结果"""
|
||||
|
||||
hook_name: str
|
||||
success: bool
|
||||
continue_execution: bool = True # False 表示中断执行
|
||||
modified_input: Any = None # 修改后的输入
|
||||
modified_output: Any = None # 修改后的输出
|
||||
error: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionContext:
|
||||
"""工具执行上下文"""
|
||||
|
||||
tool_name: str
|
||||
tool_input: dict[str, Any]
|
||||
user_id: str | None = None
|
||||
session_id: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# 执行结果(由 HookExecutor 填充)
|
||||
result: Any = None
|
||||
error: Exception | None = None
|
||||
start_time: float | None = None
|
||||
end_time: float | None = None
|
||||
|
||||
|
||||
# Hook 处理函数类型
|
||||
HookHandler = Callable[[ExecutionContext, HookDefinition], HookResult]
|
||||
|
||||
# Pre-hook: 在工具执行前调用,可以修改输入或决定是否跳过
|
||||
PreToolHook = Callable[[ExecutionContext], tuple[bool, dict[str, Any] | None]]
|
||||
# post-hook: 在工具执行后调用,可以修改输出
|
||||
PostToolHook = Callable[[ExecutionContext, Any], Any]
|
||||
# Error hook: 在工具出错时调用
|
||||
ErrorToolHook = Callable[[ExecutionContext, Exception], HookResult | None]
|
||||
# Skip hook: 决定是否跳过工具执行
|
||||
SkipToolHook = Callable[[ExecutionContext], bool]
|
||||
Reference in New Issue
Block a user