Skip to content

feat: 新增工具调用拦截确认机制#103

Open
PersonalViolet wants to merge 2 commits into
jjyaoao:mainfrom
PersonalViolet:tools_interceptor
Open

feat: 新增工具调用拦截确认机制#103
PersonalViolet wants to merge 2 commits into
jjyaoao:mainfrom
PersonalViolet:tools_interceptor

Conversation

@PersonalViolet

@PersonalViolet PersonalViolet commented Jul 17, 2026

Copy link
Copy Markdown

工具调用拦截器 — Human-in-the-Loop 确认机制

概述

工具调用拦截器(ToolInterceptor)在工具实际执行之前插入一个可配置的确认节点,允许开发者在运行时决定是否放行某次工具调用。适用于文件写入确认、高危操作审批、付费 API 次数控制等场景。

核心价值:

  • 默认 AlwaysAllowInterceptor 直接放行,完全向后兼容
  • 一行代码切换确认策略,无需改动 Agent 或 Tool 代码
  • 同步和异步路径全覆盖
  • 支持缓存决策,避免重复打扰

快速开始

from hello_agents import HelloAgentsLLM, SimpleAgent, ToolRegistry
from hello_agents.tools import (
    ReadTool, WriteTool, CalculatorTool,
    ConsoleConfirmInterceptor,
)

llm = HelloAgentsLLM()
registry = ToolRegistry()
registry.register_tool(ReadTool())
registry.register_tool(WriteTool())
registry.register_tool(CalculatorTool())

agent = ReactAgent("assistant", llm, tool_registry=registry)

# 一行代码启用控制台确认
agent.set_tool_interceptor(
    ConsoleConfirmInterceptor(
        whitelist={"Read", "python_calculator"},  # 这些工具不弹确认
        timeout=60.0,
    )
)

agent.run("读取 README.md,然后把版本号改为 2.0.0")
# ReadTool  → 自动放行(白名单)
# WriteTool → 打印参数并等待控制台输入 y/n

架构

                        ToolInterceptor (ABC)
                        ├── intercept()       同步拦截,所有子类必须实现
                        ├── aintercept()      异步拦截,默认在线程池中运行 intercept()
                        └── reset()           重置内部状态

    ┌──────────────────┼──────────────────┐
    │                  │                  │
AlwaysAllow      ConsoleConfirm     CallbackInterceptor
(默认放行)       (控制台 y/n)       (自定义回调)
                    │
             SessionConfirm
             (缓存决策,同参数只问一次)

Closes #102

Copilot AI review requested due to automatic review settings July 17, 2026 14:41
@PersonalViolet PersonalViolet changed the title feat: 新增工具调用拦截确认机制 feat: 新增工具调用拦截确认机制 Closes #102 Jul 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 为 HelloAgents 的工具执行链路新增可插拔的 ToolInterceptor(Human-in-the-Loop) 能力,使工具在真正执行前可以被统一拦截、确认或拒绝,并覆盖同步/异步两条执行路径;同时补充了拦截器相关的测试、示例与可选依赖(anthropic / gemini)。

Changes:

  • 新增 hello_agents.tools.interceptor:提供 AlwaysAllow / ConsoleConfirm / SessionConfirm / Callback 等拦截策略与异步支持。
  • AgentToolRegistryReActAgent 的工具执行路径中接入拦截器;并将拦截器导出到包入口与 hello_agents.tools
  • 增加拦截器测试与演示脚本;更新 pyproject.toml 的 pytest asyncio 配置;更新 lockfile 与可选依赖。

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
uv.lock 锁定新增可选依赖(anthropic、google-genai 等)及其传递依赖版本。
pyproject.toml pytest 配置新增 asyncio_mode = "auto"
hello_agents/tools/interceptor.py 新增拦截器抽象与多种实现,支持同步/异步与会话缓存。
hello_agents/tools/registry.py ToolRegistry 执行前增加拦截器检查,并新增异步执行入口。
hello_agents/core/agent.py Agent 层工具执行前增加拦截器检查,并提供异步工具执行方法。
hello_agents/agents/react_agent.py ReActAgent 并行工具执行前增加异步拦截器检查。
hello_agents/tools/init.py 导出拦截器相关类型与实现。
hello_agents/init.py 顶层包导出拦截器相关类型与实现。
tests/test_tool_interceptor.py 新增拦截器系统与异步路径的单测、以及与 ToolRegistry 的集成测试。
examples/tool_interceptor_demo.py 新增拦截器演示脚本(控制台确认、会话缓存、回调等)。
hello_agents/core/config.py 新增拦截器相关配置字段(当前仅定义)。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +175 to +199
# 检查拦截器(Human-in-the-loop)
import json as _json
params_for_intercept = input_text
if isinstance(input_text, str):
try:
params_for_intercept = _json.loads(input_text)
except _json.JSONDecodeError:
params_for_intercept = {"input": input_text}
elif isinstance(input_text, dict):
params_for_intercept = input_text
else:
params_for_intercept = {"input": str(input_text)}

interceptor_result = self.interceptor.intercept(
tool_name=name,
parameters=params_for_intercept,
context={"tool_registry": type(self).__name__}
)
if interceptor_result.is_denied:
return ToolResponse.error(
code=ToolErrorCode.PERMISSION_DENIED,
message=f"工具 '{name}' 被拦截器拒绝: {interceptor_result.reason}",
context={"tool_name": name, "interceptor_reason": interceptor_result.reason}
)

Comment on lines +229 to +253
# 检查拦截器(异步版本 - Human-in-the-loop)
import json as _json
params_for_intercept = input_text
if isinstance(input_text, str):
try:
params_for_intercept = _json.loads(input_text)
except _json.JSONDecodeError:
params_for_intercept = {"input": input_text}
elif isinstance(input_text, dict):
params_for_intercept = input_text
else:
params_for_intercept = {"input": str(input_text)}

interceptor_result = await self.interceptor.aintercept(
tool_name=name,
parameters=params_for_intercept,
context={"tool_registry": type(self).__name__}
)
if interceptor_result.is_denied:
return ToolResponse.error(
code=ToolErrorCode.PERMISSION_DENIED,
message=f"工具 '{name}' 被拦截器拒绝: {interceptor_result.reason}",
context={"tool_name": name, "interceptor_reason": interceptor_result.reason}
)

Comment on lines +700 to +710
# 0. 检查拦截器(Human-in-the-loop)
interceptor_result = self._tool_interceptor.intercept(
tool_name=tool_name,
parameters=arguments,
context={
"agent_name": self.name,
"agent_type": self.__class__.__name__,
}
)
if interceptor_result.is_denied:
return f"❌ 工具调用被拦截: {interceptor_result.reason}"
Comment on lines +768 to +778
# 0. 异步检查拦截器(Human-in-the-loop - 不阻塞事件循环)
interceptor_result = await self._tool_interceptor.aintercept(
tool_name=tool_name,
parameters=arguments,
context={
"agent_name": self.name,
"agent_type": self.__class__.__name__,
}
)
if interceptor_result.is_denied:
return f"❌ 工具调用被拦截: {interceptor_result.reason}"
Comment on lines +145 to +149
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self.intercept(tool_name, parameters, context)
)
Comment on lines +331 to +335
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: self._callback(tool_name, parameters, context)
)
Comment on lines +342 to +360
def test_registry_whitelist_tool_bypasses(self):
"""白名单工具应该绕过拦截器"""
from hello_agents.tools.registry import ToolRegistry

registry = ToolRegistry(
interceptor=ConsoleConfirmInterceptor(
whitelist={"CalculatorTool"}
)
)
# 白名单工具不应被拦截(在拦截前应返回 allow)
# 但这里工具不存在,所以最终会返回 NOT_FOUND
# 我们需要验证拦截器没有拒绝它
# 实际上 calculator 在注册表中不存在,所以会走到 NOT_FOUND
# 但拦截器会先被检查
result = registry.execute_tool("NonExistentTool", "test")
# 如果不在白名单中,console interceptor 会在 input() 上阻塞
# 所以这里只测白名单路径
pass

Comment on lines +93 to +95
# 注意:SessionConfirm 内部使用 ConsoleConfirm 作为 delegate,
# 在实际运行中第二次不会弹确认框,但这里 delegate 会再次触发 input()
print(f" 缓存条目数: {len(agent_interceptor._cache)}")
Comment on lines +700 to +710
# 0. 检查拦截器(Human-in-the-loop)
interceptor_result = self._tool_interceptor.intercept(
tool_name=tool_name,
parameters=arguments,
context={
"agent_name": self.name,
"agent_type": self.__class__.__name__,
}
)
if interceptor_result.is_denied:
return f"❌ 工具调用被拦截: {interceptor_result.reason}"
Comment on lines +420 to +424
def _make_key(self, tool_name: str, parameters: Dict[str, Any]) -> str:
"""生成缓存键:(工具名, 参数哈希)"""
params_json = json.dumps(parameters, sort_keys=True, ensure_ascii=False)
params_hash = hashlib.sha256(params_json.encode()).hexdigest()[:16]
return f"{tool_name}:{params_hash}"
@PersonalViolet PersonalViolet changed the title feat: 新增工具调用拦截确认机制 Closes #102 feat: 新增工具调用拦截确认机制 Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 工具调用拦截确认机制(Human-in-the-loop)

2 participants