refactor: unify skills+cli runtime and streamline ag-ui flow

This commit is contained in:
qzl
2026-04-22 17:09:37 +08:00
parent eeed737949
commit 4d55df45ab
111 changed files with 4858 additions and 3264 deletions
@@ -0,0 +1,62 @@
from __future__ import annotations
import pytest
from core.agentscope.tools.cli.models import CliCommand, CliCommandResult
from core.agentscope.tools.cli.router import CommandRouter
@pytest.mark.asyncio
async def test_router_register_and_dispatch() -> None:
router = CommandRouter()
async def mock_handler(request: CliCommand) -> CliCommandResult:
return CliCommandResult(ok=True, command=request.command, subcommand=request.subcommand, data={"name": request.args["name"]})
router.register(command="test", subcommand="run", handler=mock_handler)
assert ("test", "run") in router.command_pairs
result = await router.dispatch(CliCommand(command="test", subcommand="run", args={"name": "demo"}, owner_id="u1"))
assert result.ok is True
assert result.data == {"name": "demo"}
@pytest.mark.asyncio
async def test_router_unknown_command() -> None:
router = CommandRouter()
result = await router.dispatch(CliCommand(command="unknown", subcommand="run", args={}, owner_id="u1"))
assert result.ok is False
assert result.error is not None
assert result.error.code == "UNKNOWN_COMMAND"
@pytest.mark.asyncio
async def test_router_handler_exception() -> None:
router = CommandRouter()
async def failing_handler(request: CliCommand) -> CliCommandResult:
del request
raise ValueError("intentional error")
router.register(command="fail", subcommand="run", handler=failing_handler)
result = await router.dispatch(CliCommand(command="fail", subcommand="run", args={}, owner_id="u1"))
assert result.ok is False
assert result.error is not None
assert result.error.code == "HANDLER_ERROR"
def test_router_duplicate_register() -> None:
router = CommandRouter()
async def handler1(request: CliCommand) -> CliCommandResult:
return CliCommandResult(ok=True, command=request.command, subcommand=request.subcommand)
async def handler2(request: CliCommand) -> CliCommandResult:
return CliCommandResult(ok=True, command=request.command, subcommand=request.subcommand)
router.register(command="cmd", subcommand="one", handler=handler1)
with pytest.raises(ValueError, match="already registered"):
router.register(command="cmd", subcommand="one", handler=handler2)
@@ -0,0 +1,73 @@
from __future__ import annotations
from core.agentscope.tools.tool_postprocessor import postprocess_tool_output
from schemas.agent.runtime_models import ToolAgentOutput, ToolStatus
def _make_tool_output(
*,
command: str,
subcommand: str,
status: ToolStatus,
data: dict | None = None,
) -> ToolAgentOutput:
return ToolAgentOutput(
tool_name="project_cli",
tool_call_id="test_call_id",
tool_call_args={"command": command, "subcommand": subcommand, "args": {}},
status=status,
result={"command": command, "subcommand": subcommand, "data": data or {}},
error=None,
ui_hints=None,
)
def test_postprocess_calendar_read_success() -> None:
output = _make_tool_output(command="calendar", subcommand="read", status=ToolStatus.SUCCESS, data={"total": 5, "items": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "calendar_event_list"
assert processed.ui_hints["total"] == 5
def test_postprocess_calendar_write_partial() -> None:
output = _make_tool_output(command="calendar", subcommand="write", status=ToolStatus.PARTIAL, data={"status": "partial", "results": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "calendar_batch_result"
assert processed.ui_hints["status"] == "partial"
def test_postprocess_contacts_lookup_success() -> None:
output = _make_tool_output(command="contacts", subcommand="lookup", status=ToolStatus.SUCCESS, data={"friends_count": 3, "friends": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "contact_list"
assert processed.ui_hints["friends_count"] == 3
def test_postprocess_memory_forget_success() -> None:
output = _make_tool_output(command="memory", subcommand="forget", status=ToolStatus.SUCCESS, data={"status": "success", "forgotten": 5})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["forgotten"] == 5
def test_postprocess_failure_no_ui_hints() -> None:
output = _make_tool_output(command="calendar", subcommand="read", status=ToolStatus.FAILURE, data=None)
processed = postprocess_tool_output(output)
assert processed.ui_hints is None
def test_postprocess_unknown_command_no_ui_hints() -> None:
output = _make_tool_output(command="unknown", subcommand="run", status=ToolStatus.SUCCESS, data={"data": "test"})
processed = postprocess_tool_output(output)
assert processed.ui_hints is None
def test_postprocess_preserves_existing_ui_hints() -> None:
output = _make_tool_output(command="calendar", subcommand="read", status=ToolStatus.SUCCESS, data={"total": 5})
output = output.model_copy(update={"ui_hints": {"view": "custom_view", "custom": True}})
processed = postprocess_tool_output(output)
assert processed.ui_hints["view"] == "custom_view"
assert processed.ui_hints["custom"] is True
@@ -0,0 +1,61 @@
import asyncio
from core.agentscope.tools.internal.project_cli import PROJECT_CLI_TOOL_NAME
from core.agentscope.tools.internal.view_skill_file import VIEW_SKILL_FILE_TOOL_NAME
from core.agentscope.tools.internal import make_view_skill_file_wrapper
from core.agentscope.tools.toolkit import build_toolkit
from schemas.agent.skill_config import SkillName
def test_skill_names_is_complete() -> None:
expected = {"calendar", "contacts", "memory"}
assert {skill.value for skill in SkillName} == expected
def test_validate_rejects_unknown_skill() -> None:
from core.agentscope.tools.toolkit import _validate_enabled_skill_names
try:
_validate_enabled_skill_names({"calendar", "nonexistent"})
assert False, "should have raised"
except ValueError as exc:
assert "nonexistent" in str(exc)
def test_validate_accepts_known_skills() -> None:
from core.agentscope.tools.toolkit import _validate_enabled_skill_names
result = _validate_enabled_skill_names({"calendar", "contacts"})
assert result == {"calendar", "contacts"}
def test_build_toolkit_registers_project_cli() -> None:
toolkit = build_toolkit()
schemas = toolkit.get_json_schemas()
assert {item["function"]["name"] for item in schemas} == {
PROJECT_CLI_TOOL_NAME,
VIEW_SKILL_FILE_TOOL_NAME,
}
def test_view_skill_file_rejects_path_outside_enabled_skill_dirs() -> None:
wrapper = make_view_skill_file_wrapper(enabled_skill_names={"calendar"})
response = asyncio.run(
wrapper(file_path="/tmp/not-allowed.txt", ranges=None),
)
assert response.content
block = response.content[0]
text = block["text"] if isinstance(block, dict) else block.text
assert "ACCESS_DENIED" in text or "access denied" in text.lower()
def test_view_skill_file_reads_enabled_skill_file() -> None:
wrapper = make_view_skill_file_wrapper(enabled_skill_names={"calendar"})
response = asyncio.run(wrapper(file_path="calendar/SKILL.md", ranges=[1, 10]))
assert response.content
block = response.content[0]
text = block["text"] if isinstance(block, dict) else block.text
assert "Calendar Skill" in text or "name: calendar" in text