mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 05:15:22 +08:00
75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
"""Tests for the skills recall nudge injected into the system prompt.
|
|
|
|
Skills are surfaced by the host as ``ctx.resources.skills`` facts; the runner
|
|
renders a name+description ``<available_skills>`` index (Agent Skills
|
|
convention) so the model reliably considers calling ``activate``. Full skill
|
|
instructions stay out of context until activation (progressive disclosure).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from langbot_plugin.api.entities.builtin.runner.resources import (
|
|
AgentResources,
|
|
SkillResource,
|
|
)
|
|
|
|
from pkg.messages import build_skills_system_message
|
|
|
|
|
|
def _ctx(skills: list[SkillResource]) -> SimpleNamespace:
|
|
return SimpleNamespace(resources=AgentResources(skills=skills))
|
|
|
|
|
|
def test_returns_none_when_no_skills() -> None:
|
|
assert build_skills_system_message(_ctx([])) is None
|
|
assert build_skills_system_message(SimpleNamespace(resources=None)) is None
|
|
assert build_skills_system_message(SimpleNamespace()) is None
|
|
|
|
|
|
def test_renders_available_skills_xml_with_activate_instruction() -> None:
|
|
msg = build_skills_system_message(
|
|
_ctx(
|
|
[
|
|
SkillResource(skill_name="pdf-tools", description="Work with PDF files."),
|
|
SkillResource(skill_name="brave-search", description="Web search via Brave."),
|
|
]
|
|
)
|
|
)
|
|
assert msg is not None
|
|
assert msg.role == "system"
|
|
content = msg.content
|
|
|
|
# Progressive-disclosure nudge points at the activate tool, not a file path.
|
|
assert "`activate`" in content
|
|
assert "<available_skills>" in content and "</available_skills>" in content
|
|
assert "<name>pdf-tools</name>" in content
|
|
assert "<description>Work with PDF files.</description>" in content
|
|
assert "<name>brave-search</name>" in content
|
|
# Order preserved (host already sorts), and no full instructions leaked.
|
|
assert content.index("pdf-tools") < content.index("brave-search")
|
|
|
|
|
|
def test_skill_without_description_still_listed() -> None:
|
|
msg = build_skills_system_message(_ctx([SkillResource(skill_name="bare-skill")]))
|
|
assert msg is not None
|
|
assert "<name>bare-skill</name>" in msg.content
|
|
assert "<description>" not in msg.content
|
|
|
|
|
|
def test_xml_special_characters_are_escaped() -> None:
|
|
msg = build_skills_system_message(_ctx([SkillResource(skill_name="x", description='a < b & c "d" <tag>')]))
|
|
assert msg is not None
|
|
assert "<" in msg.content and "&" in msg.content and """ in msg.content
|
|
# Raw description markup must not pass through verbatim.
|
|
assert "< b & c" not in msg.content
|
|
|
|
|
|
def test_dict_shaped_skills_are_supported() -> None:
|
|
# Defensive: accept dict-shaped skill facts as well as SkillResource models.
|
|
ctx = SimpleNamespace(resources=SimpleNamespace(skills=[{"skill_name": "d", "description": "x"}]))
|
|
msg = build_skills_system_message(ctx)
|
|
assert msg is not None
|
|
assert "<name>d</name>" in msg.content
|