Files
wehub-resource-sync c56bef871b
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

249 lines
12 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import threading
from concurrent.futures import ThreadPoolExecutor
import pytest
from haystack.components.agents.state import State
from haystack.components.agents.tool_calling import _run_tool
from haystack.dataclasses import ChatMessage, FileContent, ImageContent, ToolCall
from haystack.skill_stores.file_system.skill_store import FileSystemSkillStore
from haystack.tools import SkillToolset, Tool
from haystack.tools.errors import ToolInvocationError
def _get_tool(toolset, name):
"""Warm up the toolset and return its tool with the given name."""
toolset.warm_up()
return next(t for t in toolset if t.name == name)
def _write_skill(skills_dir, name, description=None, body="Instructions.", files=None):
skill_dir = skills_dir / name
skill_dir.mkdir(parents=True)
frontmatter = f"---\nname: {name}\n"
if description is not None:
frontmatter += f"description: {description}\n"
frontmatter += "---\n"
(skill_dir / "SKILL.md").write_text(frontmatter + body, encoding="utf-8")
for rel_path, content in (files or {}).items():
target = skill_dir / rel_path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
return skill_dir
class TestSkillToolset:
def test_tools_present_before_warm_up_without_io(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
# The (static) tool set is available immediately, with no store access required.
assert toolset._is_warmed_up is False
assert len(toolset) == 2
assert {t.name for t in toolset} == {"load_skill", "read_skill_file"}
assert "load_skill" in toolset
assert toolset._is_warmed_up is False
def test_scans_skills_on_warm_up(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
_write_skill(tmp_path, "excel", description="Use to edit spreadsheets.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
# The catalog is only scanned on warm_up.
assert toolset._is_warmed_up is False
toolset.warm_up()
assert toolset._is_warmed_up is True
assert set(toolset.skills) == {"pdf-forms", "excel"}
assert toolset.skills["pdf-forms"].description == "Use to fill PDF forms."
def test_skills_property_warms_up_lazily(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
# Accessing `skills` without an explicit warm_up triggers it.
assert set(toolset.skills) == {"pdf-forms"}
assert toolset._is_warmed_up is True
def test_warm_up_is_idempotent(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
toolset.warm_up()
toolset.warm_up()
assert set(toolset.skills) == {"pdf-forms"}
def test_warm_up_warms_up_the_store(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
store = FileSystemSkillStore(tmp_path)
toolset = SkillToolset(store)
toolset.warm_up()
assert store._is_warmed_up is True
def test_concurrent_warm_up(self, tmp_path):
# Concurrent first use (e.g. parallel requests hitting a shared Agent) must produce a complete,
# consistent catalog in every thread.
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
_write_skill(tmp_path, "excel", description="Use to edit spreadsheets.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
num_threads = 8
barrier = threading.Barrier(num_threads)
def warm_up_and_list():
barrier.wait()
toolset.warm_up()
return set(toolset.skills)
with ThreadPoolExecutor(max_workers=num_threads) as executor:
results = list(executor.map(lambda _: warm_up_and_list(), range(num_threads)))
assert all(result == {"pdf-forms", "excel"} for result in results)
assert "- pdf-forms: Use to fill PDF forms." in toolset._load_skill_tool.description
def test_add_is_not_supported(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
extra = Tool(name="extra", description="d", parameters={"type": "object", "properties": {}}, function=len)
with pytest.raises(NotImplementedError, match="does not support adding tools"):
toolset.add(extra)
def test_concat_is_not_supported(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
extra = Tool(name="extra", description="d", parameters={"type": "object", "properties": {}}, function=len)
with pytest.raises(NotImplementedError, match="does not support concatenation"):
_ = toolset + extra
def test_accepts_skill_store_instance(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
store = FileSystemSkillStore(tmp_path)
toolset = SkillToolset(store)
toolset.warm_up()
assert set(toolset.skills) == {"pdf-forms"}
assert toolset._store is store
def test_load_skill_description_lists_skills(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
load_skill = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "load_skill")
assert "Available skills:" in load_skill.description
assert "- pdf-forms: Use to fill PDF forms." in load_skill.description
def test_load_skill_description_when_empty(self, tmp_path):
load_skill = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "load_skill")
assert "No skills are currently available." in load_skill.description
def test_load_skill_returns_body_and_manifest(self, tmp_path):
_write_skill(
tmp_path,
"pdf-forms",
description="Use to fill PDF forms.",
body="Step 1. Do the thing.",
files={"reference/forms.md": "details"},
)
load_skill = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "load_skill")
result = load_skill.invoke(name="pdf-forms")
assert "Step 1. Do the thing." in result
assert "reference/forms.md" in result
def test_load_skill_unknown_raises(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
load_skill = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "load_skill")
# The error propagates (wrapped by Tool.invoke) so the Agent can apply its tool-failure policy.
with pytest.raises(ToolInvocationError, match="Unknown skill 'nope'"):
load_skill.invoke(name="nope")
def test_read_skill_file(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="d", files={"reference/forms.md": "form details"})
read = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "read_skill_file")
assert read.invoke(name="pdf-forms", path="reference/forms.md") == "form details"
def test_read_skill_file_returns_image_as_content_part(self, tmp_path):
skill_dir = _write_skill(tmp_path, "pdf-forms", description="d")
(skill_dir / "logo.png").write_bytes(b"\x89PNG\r\n\x1a\n\x00\xff\xfe")
read = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "read_skill_file")
# raw_result keeps the ImageContent intact (no string conversion) and the tool wraps it in a list so it
# rides back as a multimodal tool-result content part.
assert read.outputs_to_string == {"raw_result": True}
result = read.invoke(name="pdf-forms", path="logo.png")
assert isinstance(result, list)
assert isinstance(result[0], ImageContent)
def test_read_skill_file_returns_pdf_as_content_part(self, tmp_path):
skill_dir = _write_skill(tmp_path, "pdf-forms", description="d")
(skill_dir / "guide.pdf").write_bytes(b"%PDF-1.4 fake pdf bytes")
read = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "read_skill_file")
result = read.invoke(name="pdf-forms", path="guide.pdf")
assert isinstance(result, list)
assert isinstance(result[0], FileContent)
def test_read_skill_file_in_agent_loop_yields_expected_tool_message_parts(self, tmp_path):
# Use the Agent tool-execution step (`_run_tool`, what the Agent loop calls) to check that multimodal content
# from `read_skill_file` comes back as expected in the tool-result messages.
skill_dir = _write_skill(tmp_path, "pdf-forms", description="d", files={"reference/forms.md": "form details"})
(skill_dir / "logo.png").write_bytes(b"\x89PNG\r\n\x1a\n\x00\xff\xfe") # PNG magic header + non-UTF-8 byte
(skill_dir / "guide.pdf").write_bytes(b"%PDF-1.4 fake pdf bytes") # minimal PDF header
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
tool_calls = [
ToolCall("read_skill_file", {"name": "pdf-forms", "path": "reference/forms.md"}, id="text"),
ToolCall("read_skill_file", {"name": "pdf-forms", "path": "logo.png"}, id="image"),
ToolCall("read_skill_file", {"name": "pdf-forms", "path": "guide.pdf"}, id="pdf"),
]
tool_messages, _ = _run_tool(
messages=[ChatMessage.from_assistant(tool_calls=tool_calls)], state=State(schema={}), tools=toolset
)
assert len(tool_messages) == 3
results = {}
for message in tool_messages:
assert message.is_from("tool")
tool_result = message.tool_call_results[0]
assert tool_result.error is False
results[tool_result.origin.id] = tool_result.result
# Text comes back as a plain string; images/PDFs as a one-element list of the matching content part.
assert results["text"] == "form details"
assert isinstance(results["image"], list) and isinstance(results["image"][0], ImageContent)
assert results["image"][0].mime_type == "image/png"
assert isinstance(results["pdf"], list) and isinstance(results["pdf"][0], FileContent)
assert results["pdf"][0].mime_type == "application/pdf"
def test_read_skill_file_blocks_traversal(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="d")
(tmp_path / "secret.txt").write_text("top secret")
read = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "read_skill_file")
with pytest.raises(ToolInvocationError, match="outside the skill directory") as exc:
read.invoke(name="pdf-forms", path="../secret.txt")
assert "top secret" not in str(exc.value)
def test_read_skill_file_missing(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="d")
read = _get_tool(SkillToolset(FileSystemSkillStore(tmp_path)), "read_skill_file")
with pytest.raises(ToolInvocationError, match="not found"):
read.invoke(name="pdf-forms", path="nope.md")
def test_to_dict_and_from_dict(self, tmp_path):
_write_skill(tmp_path, "pdf-forms", description="Use to fill PDF forms.")
toolset = SkillToolset(FileSystemSkillStore(tmp_path))
data = toolset.to_dict()
assert data == {
"type": "haystack.tools.skills.skill_toolset.SkillToolset",
"data": {
"store": {
"type": "haystack.skill_stores.file_system.skill_store.FileSystemSkillStore",
"init_parameters": {"skills_dir": str(tmp_path)},
}
},
}
restored = SkillToolset.from_dict(data)
restored.warm_up()
assert set(restored.skills) == {"pdf-forms"}