Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

409 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""Test cases for Toolkit skill-related functionality."""
import json
import os
import tempfile
from unittest.async_case import IsolatedAsyncioTestCase
from utils import AnyString
from agentscope.skill import SkillLoaderBase, Skill
from agentscope.tool import Toolkit, ToolChunk, ToolResponse, ToolGroup
from agentscope.message import ToolCallBlock
from agentscope.state import AgentState
def _make_skill(
name: str,
description: str = "desc",
dir_: str = "/tmp",
) -> Skill:
"""Helper function to create a Skill object for testing."""
return Skill(
name=name,
description=description,
dir=dir_,
markdown="",
updated_at=0.0,
)
class MockSkillLoader(SkillLoaderBase):
"""A mock skill loader for testing."""
def __init__(self, skills: list[Skill]) -> None:
self._skills = skills
async def list_skills(self) -> list[Skill]:
"""Return a list of all skills."""
return self._skills
class ToolkitSkillTest(IsolatedAsyncioTestCase):
"""Test cases for Toolkit skill functionality."""
async def test_init_with_various_types(self) -> None:
"""Test Toolkit initialization with str path, SkillLoaderBase, and
direct Skill objects, then assert get_skill_instructions output."""
with tempfile.TemporaryDirectory() as skill_dir:
# Create a minimal SKILL.md so LocalSkillLoader can load it
skill_md_path = os.path.join(skill_dir, "SKILL.md")
with open(skill_md_path, "w", encoding="utf-8") as f:
f.write(
"---\n"
"name: path_skill\n"
"description: A skill loaded from a path\n"
"---\n\n"
"# Path Skill\n",
)
loader_skill = _make_skill(
"loader_skill",
description="A skill from loader",
dir_="/loader/dir",
)
direct_skill = _make_skill(
"direct_skill",
description="A directly provided skill",
dir_="/direct/dir",
)
toolkit = Toolkit(
skills_or_loaders=[
skill_dir, # str -> LocalSkillLoader
MockSkillLoader([loader_skill]), # SkillLoaderBase
direct_skill, # Skill directly
],
)
result = await toolkit.get_skill_instructions()
self.assertEqual(
result,
# pylint: disable=line-too-long
f"""<agent-skills>
Skills are a collection of instructions, scripts, and resources to extend your capabilities.
**IMPORTANT**: Skills are NOT tools, and you cannot call a skill directly. To use a skill, you MUST use the `Skill` tool to read the skill's full instructions, and then follow those instructions to use the tools and resources provided by the skill.
# Available Skills:
<skill>
<name>path_skill</name>
<description>A skill loaded from a path</description>
<dir>{skill_dir}</dir>
</skill>
<skill>
<name>loader_skill</name>
<description>A skill from loader</description>
<dir>/loader/dir</dir>
</skill>
<skill>
<name>direct_skill</name>
<description>A directly provided skill</description>
<dir>/direct/dir</dir>
</skill>
</agent-skills>""", # noqa: E501
)
async def test_get_skill_instructions_no_skills(self) -> None:
"""Test that get_skill_instructions returns None when no skills
registered."""
toolkit = Toolkit()
result = await toolkit.get_skill_instructions()
self.assertIsNone(result)
async def test_get_skill_instructions_multiple_loaders(self) -> None:
"""Test that get_skill_instructions aggregates skills from multiple
loaders."""
loader1 = MockSkillLoader([_make_skill("skill_x")])
loader2 = MockSkillLoader([_make_skill("skill_y")])
toolkit = Toolkit(skills_or_loaders=[loader1, loader2])
result = await toolkit.get_skill_instructions()
self.assertEqual(
result,
# pylint: disable=line-too-long
"""<agent-skills>
Skills are a collection of instructions, scripts, and resources to extend your capabilities.
**IMPORTANT**: Skills are NOT tools, and you cannot call a skill directly. To use a skill, you MUST use the `Skill` tool to read the skill's full instructions, and then follow those instructions to use the tools and resources provided by the skill.
# Available Skills:
<skill>
<name>skill_x</name>
<description>desc</description>
<dir>/tmp</dir>
</skill>
<skill>
<name>skill_y</name>
<description>desc</description>
<dir>/tmp</dir>
</skill>
</agent-skills>""", # noqa: E501
)
async def test_get_skill_instructions_empty_loader(self) -> None:
"""Test that an empty loader contributes no skills."""
loader = MockSkillLoader([])
toolkit = Toolkit(skills_or_loaders=[loader])
result = await toolkit.get_skill_instructions()
self.assertIsNone(result)
async def test_get_skill_instructions_includes_tool_group_skills(
self,
) -> None:
"""Test that skill instructions include skills from custom groups."""
toolkit = Toolkit(
tool_groups=[
ToolGroup(
name="repair",
description="Repair tools",
skills_or_loaders=[
MockSkillLoader([_make_skill("repair_skill")]),
],
),
],
)
result = await toolkit.get_skill_instructions()
self.assertIsNotNone(result)
assert result is not None
self.assertIn("<name>repair_skill</name>", result)
async def test_get_skill_instructions_filters_inactive_group_skills(
self,
) -> None:
"""Test that inactive group skills are hidden from the prompt."""
toolkit = Toolkit(
skills_or_loaders=[MockSkillLoader([_make_skill("basic_skill")])],
tool_groups=[
ToolGroup(
name="repair",
description="Repair tools",
skills_or_loaders=[
MockSkillLoader([_make_skill("repair_skill")]),
],
),
],
)
inactive_result = await toolkit.get_skill_instructions(
activated_groups=[],
)
active_result = await toolkit.get_skill_instructions(
activated_groups=["repair"],
)
self.assertIsNotNone(inactive_result)
self.assertIsNotNone(active_result)
assert inactive_result is not None
assert active_result is not None
self.assertIn("<name>basic_skill</name>", inactive_result)
self.assertNotIn("<name>repair_skill</name>", inactive_result)
self.assertIn("<name>basic_skill</name>", active_result)
self.assertIn("<name>repair_skill</name>", active_result)
class ToolkitSkillViewerTest(IsolatedAsyncioTestCase):
"""Test cases for Toolkit SkillViewer functionality."""
async def test_register_skill_and_get_function_schemas(self) -> None:
"""Test that registering skills makes SkillViewer available in
function schemas."""
skill = _make_skill("test_skill", description="A test skill")
loader = MockSkillLoader([skill])
toolkit = Toolkit(skills_or_loaders=[loader])
schemas = await toolkit.get_tool_schemas()
self.assertListEqual(
schemas,
[
{
"type": "function",
"function": {
"name": "Skill",
"description": (
"Retrieve a skill within the conversation. "
"When users asks you to perform tasks, check if "
"any of the available skills match. "
"Skills provide specialized capabilities and "
"domain knowledge."
),
"parameters": {
"type": "object",
"properties": {
"skill": {
"type": "string",
"description": "The exact name of the "
"skill to view. ",
},
},
"required": ["skill"],
},
},
},
],
)
async def test_call_skill_viewer_success(self) -> None:
"""Test calling SkillViewer with an existing skill."""
skill = _make_skill(
"my_skill",
description="My test skill",
dir_="/test/dir",
)
skill.markdown = "# My Skill\nThis is the skill content."
loader = MockSkillLoader([skill])
toolkit = Toolkit(skills_or_loaders=[loader])
tool_call = ToolCallBlock(
id="test_call_1",
name="Skill",
input=json.dumps({"skill": "my_skill"}),
)
state = AgentState()
chunks = []
response = None
async for result in toolkit.call_tool(tool_call, state):
if isinstance(result, ToolChunk):
chunks.append(result)
elif isinstance(result, ToolResponse):
response = result
self.assertEqual(len(chunks), 1)
self.assertDictEqual(
chunks[0].model_dump(),
{
"content": [
{
"type": "text",
"id": AnyString(),
"text": "# My Skill\nThis is the skill content.",
},
],
"state": "running",
"is_last": True,
"metadata": {},
"id": AnyString(),
},
)
self.assertIsNotNone(response)
self.assertDictEqual(
response.model_dump(),
{
"content": [
{
"type": "text",
"id": AnyString(),
"text": "# My Skill\nThis is the skill content.",
},
],
"state": "success",
"metadata": {},
"id": "test_call_1",
},
)
async def test_call_skill_viewer_not_found(self) -> None:
"""Test calling SkillViewer with a non-existent skill."""
skill = _make_skill("existing_skill")
loader = MockSkillLoader([skill])
toolkit = Toolkit(skills_or_loaders=[loader])
tool_call = ToolCallBlock(
id="test_call_2",
name="Skill",
input=json.dumps({"skill": "non_existent_skill"}),
)
state = AgentState()
chunks = []
response = None
async for result in toolkit.call_tool(tool_call, state):
if isinstance(result, ToolChunk):
chunks.append(result)
elif isinstance(result, ToolResponse):
response = result
self.assertEqual(len(chunks), 1)
self.assertDictEqual(
chunks[0].model_dump(),
{
"content": [
{
"type": "text",
"id": AnyString(),
"text": "SkillNotFoundError: "
"Skill 'non_existent_skill' not found.",
},
],
"state": "error",
"is_last": True,
"metadata": {},
"id": AnyString(),
},
)
self.assertIsNotNone(response)
self.assertDictEqual(
response.model_dump(),
{
"content": [
{
"type": "text",
"id": AnyString(),
"text": "SkillNotFoundError: "
"Skill 'non_existent_skill' not found.",
},
],
"state": "error",
"metadata": {},
"id": "test_call_2",
},
)
async def test_skill_viewer_uses_active_tool_group_skills(self) -> None:
"""Test that activated custom-group skills expose SkillViewer."""
skill = _make_skill("repair_skill")
skill.markdown = "# Repair Skill\nUse this for repair tasks."
toolkit = Toolkit(
tool_groups=[
ToolGroup(
name="repair",
description="Repair tools",
skills_or_loaders=[MockSkillLoader([skill])],
),
],
)
schema_names = [
schema["function"]["name"]
for schema in await toolkit.get_tool_schemas(groups=["repair"])
]
self.assertIn("Skill", schema_names)
tool_call = ToolCallBlock(
id="test_call_group_skill",
name="Skill",
input=json.dumps({"skill": "repair_skill"}),
)
state = AgentState()
state.tool_context.activated_groups.append("repair")
chunks = []
async for result in toolkit.call_tool(tool_call, state):
if isinstance(result, ToolChunk):
chunks.append(result)
self.assertEqual(len(chunks), 1)
self.assertEqual(
chunks[0].content[0].text,
"# Repair Skill\nUse this for repair tasks.",
)