feat(agent): 接入 Skill Plugin 与知识库工具
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agent.permissions import PermissionMode
|
||||
from app.agent.tools import ToolExecutionContext
|
||||
from app.container import build_container
|
||||
from app.contracts import (
|
||||
AgentRunCreateRequest,
|
||||
AgentRunStatus,
|
||||
SkillStatus,
|
||||
ToolCall,
|
||||
)
|
||||
from app.extensions import ExtensionError
|
||||
from app.services import note_service
|
||||
|
||||
|
||||
def run(coroutine):
|
||||
return asyncio.run(coroutine)
|
||||
|
||||
|
||||
def test_bundled_plugin_registers_tool_and_skill_is_ready() -> None:
|
||||
async def scenario() -> None:
|
||||
container = build_container()
|
||||
|
||||
plugin = container.plugins.get("text-tools")
|
||||
skill = container.skills.get("knowledge-assistant")
|
||||
definition = container.tools.get("text.uppercase").definition
|
||||
result = await container.tools.execute(
|
||||
ToolCall(
|
||||
tool_call_id="call_uppercase",
|
||||
name="text.uppercase",
|
||||
arguments={"text": "hello plugin"},
|
||||
),
|
||||
ToolExecutionContext(run_id="run_extension_test"),
|
||||
)
|
||||
|
||||
assert plugin.enabled is True and plugin.status == "ready"
|
||||
assert skill.enabled is True and skill.status == SkillStatus.ready
|
||||
assert definition.source == "plugin"
|
||||
assert result.success is True
|
||||
assert result.output == {"text": "HELLO PLUGIN"}
|
||||
|
||||
run(scenario())
|
||||
|
||||
|
||||
def test_skill_drives_agent_and_can_call_plugin_tool() -> None:
|
||||
async def scenario() -> None:
|
||||
container = build_container()
|
||||
created = await container.agent.create_run(
|
||||
AgentRunCreateRequest(
|
||||
input='/tool text.uppercase {"text":"skill plugin"}',
|
||||
provider_id="mock",
|
||||
model="mock-1",
|
||||
skill_id="knowledge-assistant",
|
||||
)
|
||||
)
|
||||
|
||||
completed = await container.agent.wait(created.run_id)
|
||||
|
||||
assert completed.status == AgentRunStatus.completed
|
||||
assert completed.tool_results[0].success is True
|
||||
assert completed.tool_results[0].output == {"text": "SKILL PLUGIN"}
|
||||
|
||||
run(scenario())
|
||||
|
||||
|
||||
def test_plugin_disable_updates_skill_dependency_status() -> None:
|
||||
container = build_container()
|
||||
|
||||
disabled = container.plugins.disable("text-tools")
|
||||
skill = container.skills.get("knowledge-assistant")
|
||||
|
||||
assert disabled.status == "disabled"
|
||||
assert not container.tools.contains("text.uppercase")
|
||||
assert skill.status == SkillStatus.dependency_missing
|
||||
assert skill.missing_dependencies == ["text.uppercase"]
|
||||
|
||||
with pytest.raises(ExtensionError) as exc:
|
||||
container.skills.build_agent_configuration(
|
||||
"knowledge-assistant",
|
||||
container.providers.get("mock").config.capabilities,
|
||||
)
|
||||
assert exc.value.code == "SKILL_NOT_READY"
|
||||
|
||||
container.plugins.enable("text-tools")
|
||||
assert container.skills.get("knowledge-assistant").status == SkillStatus.ready
|
||||
|
||||
|
||||
def test_enabled_skill_blocks_plugin_uninstall() -> None:
|
||||
container = build_container()
|
||||
plugin = container.plugins.get("text-tools")
|
||||
dependencies = container.skills.depending_on_tools(plugin.manifest.contributes.tools)
|
||||
|
||||
with pytest.raises(ExtensionError) as exc:
|
||||
container.plugins.uninstall("text-tools", dependencies)
|
||||
|
||||
assert exc.value.code == "PLUGIN_IN_USE"
|
||||
assert exc.value.details["skills"] == ["knowledge-assistant"]
|
||||
|
||||
|
||||
def test_agent_note_search_tool_collects_citations() -> None:
|
||||
async def scenario() -> None:
|
||||
container = build_container()
|
||||
await note_service.create_note(
|
||||
title="Agent 检索",
|
||||
markdown="# Agent\n\nAgent 可以通过工具检索本地知识库。",
|
||||
folder="",
|
||||
tags=["agent"],
|
||||
)
|
||||
created = await container.agent.create_run(
|
||||
AgentRunCreateRequest(
|
||||
input='/tool notes.search {"query":"本地知识库","mode":"fts"}',
|
||||
provider_id="mock",
|
||||
model="mock-1",
|
||||
skill_id="knowledge-assistant",
|
||||
)
|
||||
)
|
||||
|
||||
completed = await container.agent.wait(created.run_id)
|
||||
|
||||
assert completed.status == AgentRunStatus.completed
|
||||
assert completed.tool_results[0].success is True
|
||||
assert completed.citations
|
||||
assert completed.citations[0].file_path == "Agent 检索.md"
|
||||
|
||||
run(scenario())
|
||||
|
||||
|
||||
def test_network_tool_requires_run_level_network_permission() -> None:
|
||||
async def scenario() -> None:
|
||||
container = build_container()
|
||||
tool = container.tools.get("system.echo")
|
||||
tool.definition.permission = "network.request"
|
||||
container.permissions.policy.set_rule("network.request", PermissionMode.allow)
|
||||
|
||||
created = await container.agent.create_run(
|
||||
AgentRunCreateRequest(
|
||||
input='/tool system.echo {"text":"network"}',
|
||||
provider_id="mock",
|
||||
model="mock-1",
|
||||
allowed_tools=["system.echo"],
|
||||
allow_network=False,
|
||||
)
|
||||
)
|
||||
completed = await container.agent.wait(created.run_id)
|
||||
|
||||
assert completed.tool_results[0].success is False
|
||||
assert completed.tool_results[0].error_code == "NETWORK_NOT_ALLOWED"
|
||||
|
||||
run(scenario())
|
||||
|
||||
|
||||
def test_skill_install_reports_missing_tool_dependency(tmp_path) -> None:
|
||||
package = tmp_path / "missing-tool-skill"
|
||||
package.mkdir()
|
||||
(package / "skill.yaml").write_text(
|
||||
"""
|
||||
id: missing-tool
|
||||
name: Missing Tool
|
||||
version: 1.0.0
|
||||
tools: [plugin.not-installed]
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
container = build_container()
|
||||
|
||||
installed = container.skills.install(package)
|
||||
|
||||
assert installed.status == SkillStatus.dependency_missing
|
||||
assert installed.missing_dependencies == ["plugin.not-installed"]
|
||||
with pytest.raises(ExtensionError) as exc:
|
||||
container.skills.enable("missing-tool")
|
||||
assert exc.value.code == "SKILL_DEPENDENCY_MISSING"
|
||||
Reference in New Issue
Block a user