refactor: 重构 schemas 结构,统一枚举定义

This commit is contained in:
qzl
2026-03-25 12:36:31 +08:00
parent 389f5248fc
commit d22ded21f8
122 changed files with 774 additions and 1456 deletions
@@ -6,7 +6,7 @@ from uuid import uuid4
import pytest
from core.agentscope.persistence.user_context_cache import UserContextCache
from schemas.user.context import (
from schemas.shared.user import (
UserContext,
parse_profile_settings,
)
@@ -1,28 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.runtime.registry_builder import build_consumer_registry
def test_build_consumer_registry_from_system_agent_configs() -> None:
registry = build_consumer_registry(
system_agent_configs={
"router": {"config": {"visibility_consumer_bit": 16}},
"worker": {"config": {"visibility_consumer_bit": 17}},
"memory": {"config": {"visibility_consumer_bit": 18}},
}
)
assert registry.resolve_agent_bit(agent_type="router") == 16
assert registry.resolve_agent_bit(agent_type="worker") == 17
def test_build_consumer_registry_rejects_duplicate_bit() -> None:
with pytest.raises(ValueError, match="duplicate visibility bit"):
build_consumer_registry(
system_agent_configs={
"router": {"config": {"visibility_consumer_bit": 16}},
"worker": {"config": {"visibility_consumer_bit": 16}},
}
)
@@ -6,8 +6,8 @@ import pytest
from ag_ui.core import RunAgentInput
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
from schemas.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.shared.user import UserContext, parse_profile_settings
class _FakePipeline:
@@ -1,24 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.runtime.pipeline_registry import build_default_pipeline_spec
def test_build_default_pipeline_spec_worker_has_two_stages() -> None:
spec = build_default_pipeline_spec(mode="worker")
assert spec.mode == "worker"
assert [item.stage_name for item in spec.stages] == ["router", "worker"]
def test_build_default_pipeline_spec_memory_has_single_stage() -> None:
spec = build_default_pipeline_spec(mode="memory")
assert spec.mode == "memory"
assert [item.stage_name for item in spec.stages] == ["memory"]
def test_build_default_pipeline_spec_rejects_unknown_mode() -> None:
with pytest.raises(ValueError, match="unsupported pipeline mode"):
build_default_pipeline_spec(mode="planner")
@@ -18,8 +18,8 @@ from schemas.agent.runtime_models import (
WorkerAgentOutputLite,
)
from schemas.agent.system_agent import AgentType
from schemas.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.shared.user import UserContext, parse_profile_settings
def _run_input() -> RunAgentInput:
@@ -7,8 +7,8 @@ import pytest
import core.agentscope.runtime.tasks as tasks_module
from schemas.agent import ToolStatus
from schemas.automation import ContextWindowMode, MessageContextConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import ContextWindowMode, MessageContextConfig
from schemas.shared.user import UserContext, parse_profile_settings
def _run_input_payload() -> dict[str, Any]:
@@ -1,250 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.schemas.agui_input import (
MAX_MESSAGES,
MAX_RUN_ID_LENGTH,
MAX_RUN_INPUT_BYTES,
MAX_TEXT_CHARS,
parse_run_input,
validate_run_request_messages_contract,
)
def _base_payload() -> dict[str, object]:
return {
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
"state": {},
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {"agent_type": "worker"},
}
def test_parse_run_input_rejects_invalid_uuid() -> None:
payload = _base_payload()
payload["threadId"] = "bad-uuid"
with pytest.raises(ValueError, match="threadId must be a valid UUID"):
parse_run_input(payload)
def test_parse_run_input_rejects_message_count_over_limit() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": f"u{i}", "role": "user", "content": "x"} for i in range(MAX_MESSAGES + 1)
]
with pytest.raises(ValueError, match="RunAgentInput.messages exceeds limit"):
parse_run_input(payload)
def test_parse_run_input_rejects_user_text_over_limit() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": "u1", "role": "user", "content": "x" * (MAX_TEXT_CHARS + 1)}
]
with pytest.raises(
ValueError, match="RunAgentInput user message text exceeds limit"
):
parse_run_input(payload)
def test_parse_run_input_rejects_payload_over_limit() -> None:
payload = _base_payload()
payload["forwardedProps"] = {"blob": "x" * MAX_RUN_INPUT_BYTES}
with pytest.raises(ValueError, match="RunAgentInput payload exceeds size limit"):
parse_run_input(payload)
def test_parse_run_input_rejects_run_id_over_limit() -> None:
payload = _base_payload()
payload["runId"] = "r" * (MAX_RUN_ID_LENGTH + 1)
with pytest.raises(ValueError, match="runId exceeds length limit"):
parse_run_input(payload)
def test_validate_run_request_messages_contract_requires_single_user_message() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": "u1", "role": "user", "content": "hello"},
{"id": "u2", "role": "user", "content": "again"},
]
run_input = parse_run_input(payload)
with pytest.raises(
ValueError,
match="RunAgentInput.messages must contain exactly one user message",
):
validate_run_request_messages_contract(run_input)
def test_validate_run_request_messages_contract_accepts_binary_url_blocks() -> None:
payload = _base_payload()
payload["messages"] = [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请分析"},
{
"type": "binary",
"mimeType": "image/png",
"url": "https://signed.example/a.png",
},
],
}
]
run_input = parse_run_input(payload)
validate_run_request_messages_contract(run_input)
def test_validate_run_request_messages_contract_rejects_binary_data_block() -> None:
payload = _base_payload()
payload["messages"] = [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请分析"},
{
"type": "binary",
"mimeType": "image/png",
"data": "aGVsbG8=",
},
],
}
]
run_input = parse_run_input(payload)
with pytest.raises(ValueError, match="binary content requires url"):
validate_run_request_messages_contract(run_input)
def test_parse_run_input_accepts_snake_case_aliases() -> None:
payload = {
"thread_id": "00000000-0000-0000-0000-000000000001",
"run_id": "run-1",
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "hello"},
{
"type": "binary",
"mime_type": "image/png",
"url": "https://signed.example/a.png",
},
],
}
],
"tools": [],
"context": [],
"forwarded_props": {"agent_type": "worker"},
}
run_input = parse_run_input(payload)
assert run_input.thread_id == "00000000-0000-0000-0000-000000000001"
assert run_input.run_id == "run-1"
validate_run_request_messages_contract(run_input)
def test_parse_run_input_accepts_client_time_forwarded_props() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
}
run_input = parse_run_input(payload)
assert run_input.forwarded_props is not None
def test_parse_run_input_rejects_invalid_client_time_timezone() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "Mars/OlympusMons",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_invalid_client_time_now_iso() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16 09:12:33",
"client_epoch_ms": 1773658353000,
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_invalid_client_time_epoch_type() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": "1773658353000",
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_unknown_forwarded_props_key() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
"unexpected": {"foo": "bar"},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_missing_forwarded_props_agent_type() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
}
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
@@ -40,6 +40,7 @@ class _FakeService:
start_at=datetime(2026, 3, 17, 9, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc),
timezone="Asia/Shanghai",
status="active",
metadata=SimpleNamespace(
location=None, color="#4F46E5", reminder_minutes=15
),
@@ -247,7 +248,7 @@ async def test_calendar_read_returns_structured_result_with_ids(
assert "total=1" in payload["result"]
assert "timezone=Asia/Shanghai" in payload["result"]
assert "description=今天下午五点的会议" in payload["result"]
assert "status=" in payload["result"]
assert "status=active" in payload["result"]
assert fake_service.created_id in payload["result"]
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}]
@@ -1,132 +0,0 @@
from __future__ import annotations
import json
from typing import Any, AsyncGenerator
import pytest
from core.agentscope.tools.tool_config import ToolApprovalConfig, ToolConfig, ToolGroup
from core.agentscope.tools.tool_middleware import create_approval_middleware
async def _next_handler(**kwargs: Any) -> AsyncGenerator[dict[str, object], None]:
async def _generator() -> AsyncGenerator[dict[str, object], None]:
yield {"ok": True, "tool_call": kwargs.get("tool_call")}
return _generator()
def _extract_error_payload(chunk: object) -> dict[str, Any]:
content = getattr(chunk, "content", None)
if not isinstance(content, list) or not content:
return {}
first_block = content[0]
text = getattr(first_block, "text", None)
if not isinstance(text, str) and isinstance(first_block, dict):
raw_text = first_block.get("text")
text = raw_text if isinstance(raw_text, str) else None
if not isinstance(text, str):
return {}
return json.loads(text)
@pytest.mark.asyncio
async def test_hitl_middleware_default_write_does_not_require_approval() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=False),
)
}
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar.write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
assert responses[0]["ok"] is True
@pytest.mark.asyncio
async def test_hitl_middleware_pending_when_tool_requires_approval() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
}
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar_write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
payload = _extract_error_payload(responses[0])
assert payload["error"]["code"] == "TOOL_PENDING_APPROVAL"
@pytest.mark.asyncio
async def test_hitl_middleware_passes_when_write_approved() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
},
approval_resolver=lambda _name, _args, _config: "approved",
)
responses = []
async for chunk in middleware(
{
"tool_call": {
"name": "calendar.write",
"input": {
"operation": "create",
"_hitl": {"approval": "required"},
},
}
},
_next_handler,
):
responses.append(chunk)
assert responses[0]["ok"] is True
sanitized_input = responses[0]["tool_call"]["input"]
assert "_hitl" not in sanitized_input
@pytest.mark.asyncio
async def test_hitl_middleware_rejected_short_circuits() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
},
approval_resolver=lambda _name, _args, _config: "rejected",
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar_write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
payload = _extract_error_payload(responses[0])
assert payload["error"]["code"] == "TOOL_REJECTED"
@@ -9,7 +9,7 @@ from agentscope.tool import ToolResponse
from core.agentscope.tools.custom import memory as memory_module
from models.memories import MemoryType
from schemas.memories.memory_content import UserMemoryContent
from schemas.domain.memory_content import UserMemoryContent
def _decode_tool_response(response: ToolResponse) -> dict[str, object]:
@@ -9,7 +9,7 @@ from core.agentscope.prompts.system_prompt import (
)
from schemas.agent.forwarded_props import ClientTimeContext
from schemas.agent.system_agent import AgentType
from schemas.user.context import UserContext, parse_profile_settings
from schemas.shared.user import UserContext, parse_profile_settings
def _build_user_context(*, timezone_name: str = "Asia/Shanghai") -> UserContext:
@@ -159,7 +159,7 @@ def test_build_system_prompt_keeps_sections_focused_without_language_duplication
def test_build_system_prompt_includes_user_memory_section_for_router() -> None:
from schemas.memories.memory_content import UserMemoryContent
from schemas.domain.memory_content import UserMemoryContent
user_memory = UserMemoryContent()
@@ -175,7 +175,7 @@ def test_build_system_prompt_includes_user_memory_section_for_router() -> None:
def test_build_system_prompt_includes_work_memory_section_for_worker() -> None:
from schemas.memories.memory_content import WorkProfileContent
from schemas.domain.memory_content import WorkProfileContent
work_memory = WorkProfileContent()
@@ -6,7 +6,7 @@ from uuid import UUID, uuid4
import pytest
from models.automation_jobs import AutomationJob as OrmAutomationJob, ScheduleType
from schemas.automation import (
from schemas.domain.automation import (
RuntimeConfig,
ScheduleConfig,
ScheduleRunAt,