feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agent.domain.system_agent_config import SystemAgentLLMConfig
|
||||
from core.agent.domain.user_context import UserAgentContext, parse_profile_settings
|
||||
from core.agentscope.runtime.config_loader import RuntimeStageConfig
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
|
||||
|
||||
def _ctx() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="alice",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _stage_config() -> dict[str, RuntimeStageConfig]:
|
||||
llm = SystemAgentLLMConfig(temperature=0.1, max_tokens=256, timeout_seconds=30)
|
||||
return {
|
||||
"intent": RuntimeStageConfig("intent", "qwen3.5-flash", "dashscope", llm),
|
||||
"execution": RuntimeStageConfig("execution", "deepseek-chat", "deepseek", llm),
|
||||
"report": RuntimeStageConfig("report", "deepseek-chat", "deepseek", llm),
|
||||
}
|
||||
|
||||
|
||||
class _FakeRunner:
|
||||
def __init__(self) -> None:
|
||||
self.intent_calls = 0
|
||||
self.execution_calls = 0
|
||||
self.report_calls = 0
|
||||
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "DIRECT_RESPONSE",
|
||||
"intent_summary": "直接问候",
|
||||
"direct_response": "你好",
|
||||
"tasks": [],
|
||||
"complexity": "simple",
|
||||
}
|
||||
self.report_calls += 1
|
||||
return {
|
||||
"assistant_text": "已完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
}
|
||||
|
||||
|
||||
class _ComplexRunner(_FakeRunner):
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: Any | None,
|
||||
) -> dict[str, Any]:
|
||||
del agent_name, system_prompt, user_prompt, toolkit
|
||||
if stage_config.stage == "intent":
|
||||
self.intent_calls += 1
|
||||
return {
|
||||
"route": "TASK_EXECUTION",
|
||||
"intent_summary": "需要写入日历",
|
||||
"direct_response": None,
|
||||
"tasks": [
|
||||
{"task_id": "t1", "title": "创建事件", "objective": "写入明天会议"}
|
||||
],
|
||||
"complexity": "complex",
|
||||
}
|
||||
if stage_config.stage == "execution":
|
||||
self.execution_calls += 1
|
||||
return {
|
||||
"task_id": "t1",
|
||||
"status": "SUCCESS",
|
||||
"execution_summary": "done",
|
||||
"execution_data": {},
|
||||
"user_feedback_needs": [],
|
||||
}
|
||||
self.report_calls += 1
|
||||
return {
|
||||
"assistant_text": "任务执行完成",
|
||||
"response_metadata": {"source": "report-agent"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_direct_response_skips_execution(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _FakeRunner()
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="你好",
|
||||
)
|
||||
|
||||
assert result.intent.route == "DIRECT_RESPONSE"
|
||||
assert result.execution is None
|
||||
assert result.report.assistant_text == "已完成"
|
||||
assert fake_runner.execution_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_complex_route_runs_execution(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_runner = _ComplexRunner()
|
||||
|
||||
async def _fake_config_loader(
|
||||
_session: AsyncSession,
|
||||
) -> dict[str, RuntimeStageConfig]:
|
||||
return _stage_config()
|
||||
|
||||
class _FakeToolkit:
|
||||
def get_json_schemas(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.read",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calendar.write",
|
||||
"description": "write",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
async def call_tool_function(self, tool_call: dict[str, Any]):
|
||||
del tool_call
|
||||
if False:
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.runtime.orchestrator.build_stage_toolkit",
|
||||
lambda **_: _FakeToolkit(),
|
||||
)
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=fake_runner,
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
result = await orchestrator.run(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token",
|
||||
user_context=_ctx(),
|
||||
user_input="帮我安排明天会议",
|
||||
)
|
||||
|
||||
assert result.intent.route == "TASK_EXECUTION"
|
||||
assert result.execution is not None
|
||||
assert result.execution.overall_status == "SUCCESS"
|
||||
assert fake_runner.execution_calls == 1
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agent.domain.system_agent_config import SystemAgentLLMConfig
|
||||
from core.agentscope.runtime.config_loader import RuntimeStageConfig
|
||||
from core.agentscope.runtime.react_runner import (
|
||||
AgentScopeReActRunner,
|
||||
_parse_json_text,
|
||||
_to_litellm_model,
|
||||
)
|
||||
|
||||
|
||||
def _stage_config() -> RuntimeStageConfig:
|
||||
return RuntimeStageConfig(
|
||||
stage="intent",
|
||||
model_code="qwen3.5-flash",
|
||||
provider_name="dashscope",
|
||||
llm_config=SystemAgentLLMConfig(
|
||||
temperature=0.1, max_tokens=128, timeout_seconds=30
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_to_litellm_model_keeps_prefixed_model() -> None:
|
||||
assert (
|
||||
_to_litellm_model(provider_name="dashscope", model_code="openai/gpt-4o")
|
||||
== "openai/gpt-4o"
|
||||
)
|
||||
|
||||
|
||||
def test_to_litellm_model_builds_prefixed_model() -> None:
|
||||
assert (
|
||||
_to_litellm_model(provider_name="dashscope", model_code="qwen3.5-flash")
|
||||
== "dashscope/qwen3.5-flash"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_json_text_supports_fenced_json() -> None:
|
||||
parsed = _parse_json_text('```json\n{"route":"DIRECT_RESPONSE"}\n```')
|
||||
assert parsed["route"] == "DIRECT_RESPONSE"
|
||||
|
||||
|
||||
def test_parse_json_text_rejects_non_json() -> None:
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
_parse_json_text("not-json")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_json_stage_wraps_json_decode_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pytest.importorskip("agentscope")
|
||||
import agentscope.agent as agent_module
|
||||
import agentscope.formatter as formatter_module
|
||||
import agentscope.memory as memory_module
|
||||
|
||||
class _FakeAgent:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def __call__(self, _msg: object) -> object:
|
||||
return SimpleNamespace(get_text_content=lambda: "not-json")
|
||||
|
||||
monkeypatch.setattr(agent_module, "ReActAgent", _FakeAgent)
|
||||
monkeypatch.setattr(formatter_module, "OpenAIChatFormatter", lambda: object())
|
||||
monkeypatch.setattr(memory_module, "InMemoryMemory", lambda: object())
|
||||
|
||||
runner = AgentScopeReActRunner()
|
||||
monkeypatch.setattr(runner, "_build_model", lambda **_: object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="agent output format invalid"):
|
||||
await runner.run_json_stage(
|
||||
stage_config=_stage_config(),
|
||||
agent_name="intent-agent",
|
||||
system_prompt="sys",
|
||||
user_prompt="user",
|
||||
toolkit=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_json_stage_wraps_runtime_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
pytest.importorskip("agentscope")
|
||||
import agentscope.agent as agent_module
|
||||
import agentscope.formatter as formatter_module
|
||||
import agentscope.memory as memory_module
|
||||
|
||||
class _FakeAgent:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
del kwargs
|
||||
|
||||
async def __call__(self, _msg: object) -> object:
|
||||
raise ValueError("boom")
|
||||
|
||||
monkeypatch.setattr(agent_module, "ReActAgent", _FakeAgent)
|
||||
monkeypatch.setattr(formatter_module, "OpenAIChatFormatter", lambda: object())
|
||||
monkeypatch.setattr(memory_module, "InMemoryMemory", lambda: object())
|
||||
|
||||
runner = AgentScopeReActRunner()
|
||||
monkeypatch.setattr(runner, "_build_model", lambda **_: object())
|
||||
|
||||
with pytest.raises(RuntimeError, match="agent execution failed"):
|
||||
await runner.run_json_stage(
|
||||
stage_config=_stage_config(),
|
||||
agent_name="intent-agent",
|
||||
system_prompt="sys",
|
||||
user_prompt="user",
|
||||
toolkit=None,
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agentscope.tools.custom import calendar as calendar_module
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_read_returns_list_payload(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
|
||||
del kwargs
|
||||
return {"type": "calendar_event_list.v1", "version": "v1", "data": {}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
calendar_module,
|
||||
"_execute_list_calendar_events",
|
||||
_fake_execute,
|
||||
)
|
||||
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_read(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
)
|
||||
assert result["type"] == "calendar_event_list.v1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_read_requires_valid_user_token(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: False)
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_read(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="bad-token",
|
||||
)
|
||||
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "UNAUTHORIZED"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_maps_event_id_for_update(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_execute(**kwargs: Any) -> dict[str, object]:
|
||||
captured.update(cast(dict[str, object], kwargs["tool_args"]))
|
||||
return {"type": "calendar_card.v1", "version": "v1", "data": {"ok": True}}
|
||||
|
||||
monkeypatch.setattr(
|
||||
calendar_module,
|
||||
"_execute_mutate_calendar_event",
|
||||
_fake_execute,
|
||||
)
|
||||
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: True)
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
operation="update",
|
||||
event_id=str(uuid4()),
|
||||
title="新标题",
|
||||
)
|
||||
assert result["type"] == "calendar_card.v1"
|
||||
assert captured["operation"] == "update"
|
||||
assert "eventId" in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_requires_preset_user_token(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
monkeypatch.setattr(calendar_module, "_verify_user_token", lambda **_: False)
|
||||
result = await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="bad-token",
|
||||
operation="create",
|
||||
)
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "UNAUTHORIZED"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_rejects_missing_event_id_for_update(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
operation="update",
|
||||
)
|
||||
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "INVALID_ARGUMENT"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_rejects_event_id_for_create(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(calendar_module, "build_tool_response", lambda payload: payload)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-abc",
|
||||
operation="create",
|
||||
event_id=str(uuid4()),
|
||||
)
|
||||
|
||||
assert result["data"]["ok"] is False
|
||||
assert result["data"]["code"] == "INVALID_ARGUMENT"
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agentscope.tools.hitl_middleware import create_hitl_middleware
|
||||
from core.agentscope.tools.tool_meta import TOOL_META, ToolMeta
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hitl_middleware_default_write_does_not_require_approval() -> None:
|
||||
middleware = create_hitl_middleware(meta_by_name=TOOL_META)
|
||||
|
||||
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(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
middleware = create_hitl_middleware(
|
||||
meta_by_name={
|
||||
"calendar.write": ToolMeta(name="calendar.write", requires_approval=True)
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.tools.hitl_middleware.build_tool_response",
|
||||
lambda payload: payload,
|
||||
)
|
||||
|
||||
responses = []
|
||||
async for chunk in middleware(
|
||||
{"tool_call": {"name": "calendar.write", "input": {"operation": "create"}}},
|
||||
_next_handler,
|
||||
):
|
||||
responses.append(chunk)
|
||||
|
||||
assert responses[0]["data"]["status"] == "pending"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hitl_middleware_passes_when_write_approved() -> None:
|
||||
middleware = create_hitl_middleware(
|
||||
meta_by_name={
|
||||
"calendar.write": ToolMeta(name="calendar.write", requires_approval=True)
|
||||
},
|
||||
approval_resolver=lambda _name, _args: "approved",
|
||||
)
|
||||
|
||||
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
|
||||
sanitized_input = responses[0]["tool_call"]["input"]
|
||||
assert "_hitl" not in sanitized_input
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hitl_middleware_rejected_short_circuits(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
middleware = create_hitl_middleware(
|
||||
meta_by_name={
|
||||
"calendar.write": ToolMeta(name="calendar.write", requires_approval=True)
|
||||
},
|
||||
approval_resolver=lambda _name, _args: "rejected",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"core.agentscope.tools.hitl_middleware.build_tool_response",
|
||||
lambda payload: payload,
|
||||
)
|
||||
|
||||
responses = []
|
||||
async for chunk in middleware(
|
||||
{"tool_call": {"name": "calendar.write", "input": {"operation": "create"}}},
|
||||
_next_handler,
|
||||
):
|
||||
responses.append(chunk)
|
||||
|
||||
assert responses[0]["data"]["status"] == "rejected"
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from core.agent.domain.user_context import UserAgentContext, parse_profile_settings
|
||||
from core.agentscope.prompts.system_prompt import build_system_prompt
|
||||
|
||||
|
||||
def _build_user_context(*, timezone_name: str = "Asia/Shanghai") -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="alice",
|
||||
bio="focus on calendars",
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": timezone_name,
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_build_system_prompt_includes_agent_role_user_context_and_time() -> None:
|
||||
prompt = build_system_prompt(
|
||||
stage="execution",
|
||||
user_context=_build_user_context(),
|
||||
tools=[
|
||||
{
|
||||
"name": "calendar.read",
|
||||
"description": "读取日程",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
{
|
||||
"name": "calendar.write",
|
||||
"description": "写入日程",
|
||||
"parameters": {"type": "object"},
|
||||
},
|
||||
],
|
||||
now_utc=datetime(2026, 3, 11, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
assert "Execution Agent" in prompt
|
||||
assert '"timezone":"Asia/Shanghai"' in prompt
|
||||
assert '"local_time":"2026-03-11T08:00:00+08:00"' in prompt
|
||||
assert "calendar.read" in prompt
|
||||
assert "calendar.write" in prompt
|
||||
assert "<!-- ENV_START -->" in prompt
|
||||
assert "<!-- TOOLS_START -->" in prompt
|
||||
|
||||
|
||||
def test_build_system_prompt_rejects_unknown_stage() -> None:
|
||||
try:
|
||||
build_system_prompt(
|
||||
stage="unknown",
|
||||
user_context=_build_user_context(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert "unknown stage" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected ValueError")
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.agentscope.prompts.tool_prompt import build_tools_prompt
|
||||
|
||||
|
||||
def test_build_tools_prompt_wraps_section_and_schema() -> None:
|
||||
prompt = build_tools_prompt(
|
||||
tools=[
|
||||
{
|
||||
"name": "calendar.read",
|
||||
"description": "读取日程",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}},
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert "<!-- TOOLS_START -->" in prompt
|
||||
assert "calendar.read" in prompt
|
||||
assert '"page":{"type":"integer"}' in prompt
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.agentscope.tools.toolkit import build_toolkit
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_toolkit_registers_calendar_tools() -> None:
|
||||
pytest.importorskip("agentscope")
|
||||
toolkit = build_toolkit(
|
||||
session=cast(AsyncSession, SimpleNamespace()),
|
||||
owner_id=uuid4(),
|
||||
user_token="token-123",
|
||||
)
|
||||
schemas = toolkit.get_json_schemas()
|
||||
names = {item["function"]["name"] for item in schemas}
|
||||
assert "calendar.read" in names
|
||||
assert "calendar.write" in names
|
||||
|
||||
write_schema = next(
|
||||
item for item in schemas if item["function"]["name"] == "calendar.write"
|
||||
)
|
||||
params = write_schema["function"]["parameters"]["properties"]
|
||||
assert "user_token" not in params
|
||||
assert "session" not in params
|
||||
assert "owner_id" not in params
|
||||
@@ -78,7 +78,7 @@ def test_verify_rejects_invalid_issuer() -> None:
|
||||
issuer="https://wrong-issuer.example.com/auth/v1",
|
||||
)
|
||||
|
||||
with pytest.raises(TokenValidationError):
|
||||
with pytest.raises(TokenValidationError, match="Token issuer mismatch"):
|
||||
verifier.verify(token)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_verify_rejects_missing_audience() -> None:
|
||||
audience=None,
|
||||
)
|
||||
|
||||
with pytest.raises(TokenValidationError):
|
||||
with pytest.raises(TokenValidationError, match="Token validation failed"):
|
||||
verifier.verify(token)
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def test_verify_rejects_rs256_token() -> None:
|
||||
issuer="https://example.supabase.co/auth/v1",
|
||||
)
|
||||
|
||||
with pytest.raises(TokenValidationError):
|
||||
with pytest.raises(TokenValidationError, match="Token algorithm invalid"):
|
||||
verifier.verify(token)
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ def test_verify_rejects_expired_token() -> None:
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
with pytest.raises(TokenValidationError):
|
||||
with pytest.raises(TokenValidationError, match="Token expired"):
|
||||
verifier.verify(token)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user