Repository navigation
fix: 修复SkillToolSet中tool_filter失效的问题(#352) #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ | |
|
|
||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
|
|
||
| from trpc_agent_sdk.skills._dynamic_toolset import SkillToolSetWithDynamicTools | ||
| from trpc_agent_sdk.skills._toolset import SkillToolSet | ||
|
|
||
|
|
@@ -28,6 +30,7 @@ def _make_ctx(): | |
|
|
||
|
|
||
| class TestSkillToolSetInit: | ||
|
|
||
| def test_default_init(self, tmp_path): | ||
| ts = SkillToolSet(paths=[str(tmp_path)]) | ||
| assert ts.name == "skill_toolset" | ||
|
|
@@ -41,6 +44,7 @@ def test_custom_repository(self): | |
|
|
||
|
|
||
| class TestSkillToolSetGetTools: | ||
|
|
||
| async def test_get_tools_returns_tools(self, tmp_path): | ||
| ts = SkillToolSet(paths=[str(tmp_path)]) | ||
| ctx = _make_ctx() | ||
|
|
@@ -74,7 +78,51 @@ async def test_get_tools_sets_metadata(self, tmp_path): | |
| ctx.agent_context.with_metadata.assert_called() | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("toolset_cls", [SkillToolSet, SkillToolSetWithDynamicTools]) | ||
| class TestSkillToolSetFiltering: | ||
|
|
||
| async def test_name_filter_applies_to_first_and_cached_calls(self, tmp_path, toolset_cls): | ||
| ts = toolset_cls( | ||
| paths=[str(tmp_path)], | ||
| tool_filter=["skill_load"], | ||
| is_include_all_tools=False, | ||
| ) | ||
|
|
||
| for _ in range(2): | ||
| tools = await ts.get_tools(_make_ctx()) | ||
| assert [tool.name for tool in tools] == ["skill_load"] | ||
|
|
||
| async def test_predicate_rechecks_current_context_without_filtering_cache(self, tmp_path, toolset_cls): | ||
|
|
||
| def predicate(tool, invocation_context): | ||
| return tool.name in invocation_context.allowed_tools | ||
|
|
||
| ts = toolset_cls( | ||
| paths=[str(tmp_path)], | ||
| tool_filter=predicate, | ||
| is_include_all_tools=False, | ||
| ) | ||
|
|
||
| for allowed_tools in ({"skill_run"}, {"skill_load"}): | ||
| ctx = _make_ctx() | ||
| ctx.allowed_tools = allowed_tools | ||
| tools = await ts.get_tools(ctx) | ||
| assert {tool.name for tool in tools} == allowed_tools | ||
|
|
||
| async def test_include_all_tools_overrides_filter(self, tmp_path, toolset_cls): | ||
| ts = toolset_cls( | ||
| paths=[str(tmp_path)], | ||
| tool_filter=["skill_load"], | ||
| is_include_all_tools=True, | ||
| ) | ||
|
|
||
| tools = await ts.get_tools(_make_ctx()) | ||
| assert "skill_load" in {tool.name for tool in tools} | ||
| assert len(tools) > 1 | ||
|
Comment on lines
+95
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 本次新增测试未覆盖修复行为的关键失败路径与边界:1) 谓词型过滤在 触发条件: 谓词过滤真实部署到 a2a 卡片、子代理等路径时的回归无法被这些测试拦截。 实际影响: 测试对修复行为给出虚假信心,静默丢工具(卡片/子代理/零工具)等真实缺陷会直接发布。 修正方向: 补充 |
||
|
|
||
|
|
||
| class TestSkillToolSetWithDynamicTools: | ||
|
|
||
| async def test_get_tools_includes_dynamic_selection_helpers(self, tmp_path): | ||
| ts = SkillToolSetWithDynamicTools(paths=[str(tmp_path)]) | ||
| ctx = _make_ctx() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,15 +144,21 @@ def repository(self) -> BaseSkillRepository: | |
| """Get the skill repository.""" | ||
| return self._repository | ||
|
|
||
| def _get_selected_tools(self, invocation_context: Optional[InvocationContext]) -> List[ToolABC]: | ||
| """Return tools selected for the current invocation.""" | ||
| if not self._tool_filter or self._is_include_all_tools: | ||
| return self._default_tools | ||
|
Comment on lines
+149
to
+150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 任一调用方对 实际影响: 一旦发生修改, 修正方向: 两条路径统一为 |
||
| return [tool for tool in self._default_tools if self._is_tool_selected(tool, invocation_context)] | ||
|
Comment on lines
+147
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 新增的 触发条件: 配置 实际影响: a2a 卡片构建与 ag_ui 长时运行检测的 修正方向: 在
Comment on lines
+149
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 运维以空列表表达“临时禁用全部工具 / 零白名单”(在访问控制场景下是合理意图)。 实际影响: 静默暴露全部内置工具(含 修正方向: 将判断改为 |
||
|
|
||
| @override | ||
| async def get_tools(self, invocation_context: Optional[InvocationContext] = None) -> List[ToolABC]: | ||
| """Get all tools from registered skills. | ||
|
|
||
| Args: | ||
| invocation_context: Optional invocation context (not used currently) | ||
| invocation_context: Optional invocation context used for filtering. | ||
|
|
||
| Returns: | ||
| List of tools from all registered skills | ||
| List of tools selected for the current invocation. | ||
| """ | ||
| if self._repo_resolver is not None: | ||
| repository = self._repo_resolver(invocation_context) | ||
|
|
@@ -167,7 +173,7 @@ async def get_tools(self, invocation_context: Optional[InvocationContext] = None | |
| if not is_exist_skill_config(agent_context): | ||
| set_skill_config(agent_context, self._skill_config) | ||
| if self._default_tools: | ||
| return self._default_tools.copy() | ||
| return self._get_selected_tools(invocation_context) | ||
|
Comment on lines
175
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 本次变更使 触发条件: 父级 实际影响: 子代理在无任何报错的情况下丢失全部 Skill 工具( 修正方向: 明确子代理场景下谓词的求值上下文——要么让
Comment on lines
175
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 问题: 触发条件: 实际影响: 按会话/租户收紧的访问控制在动态工具层被静默绕过(已缓存工具直接返回),或 preflight 的截断结果被永久缓存导致工具持续缺失,与权限控制类部署场景直接相关。 修正方向: 在 |
||
|
|
||
| tools: List[ToolABC] = [] | ||
| tools.append(self._load_tool) | ||
|
|
@@ -184,4 +190,4 @@ async def get_tools(self, invocation_context: Optional[InvocationContext] = None | |
| logger.warning("Failed to get tools from skill '%s': %s", skill_function.__name__, ex) | ||
| continue | ||
| self._default_tools.extend(tools) | ||
| return tools | ||
| return self._get_selected_tools(invocation_context) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题: 本次新增的中英文档示例
run_tool_kwargs={"require_skill_loaded": True}声称开启“必须先skill_load才能skill_run”的门控,但该嵌套写法在运行期被静默丢弃:SkillToolSet.__init__的**run_tool_kwargs(_toolset.py:81)把这个嵌套 dict 以名为run_tool_kwargs的关键字传给SkillRunTool,落入其**kwargs(_skill_run.py:384,436)并存入self._run_tool_kwargs;执行期仅将SkillRunInput.model_fields内的键写回参数(_skill_run.py:647-650),而require_skill_loaded不是输入字段,构造参数_require_skill_loaded保持默认False(已用签名级绑定推演验证)。只有examples/skills_code_review_agent/agent/tools.py:78那种把require_skill_loaded=True作为顶层关键字传入才生效。触发条件: 用户按本次新增的 zh/en
skill.md文档示例原样配置SkillToolSet。实际影响: 文档承诺的访问约束门控不生效,
skill_run无需先skill_load即可直接调用,与文档“必须同时允许skill_load”的说明矛盾且无任何警告,治理型部署按文档配置后得不到预期保护。修正方向: 将文档示例改为顶层
require_skill_loaded=True(与GovernedSkillToolSet实际用法一致),或在SkillRunTool中对_run_tool_kwargs中无法识别的键(非SkillRunInput.model_fields)打印告警以免静默失效。