feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
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
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
|
||||
def _build_user_context(owner_id: UUID) -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=owner_id,
|
||||
username="smoke-user",
|
||||
bio=None,
|
||||
settings=parse_profile_settings(
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _runtime_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", "qwen3.5-flash", "dashscope", llm),
|
||||
"report": RuntimeStageConfig("report", "qwen3.5-flash", "dashscope", llm),
|
||||
}
|
||||
|
||||
|
||||
async def _invoke_tool(
|
||||
toolkit: object,
|
||||
*,
|
||||
tool_name: str,
|
||||
tool_input: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
tool_call = {
|
||||
"type": "tool_use",
|
||||
"id": f"smoke-{tool_name}-{uuid4()}",
|
||||
"name": tool_name,
|
||||
"input": tool_input,
|
||||
}
|
||||
call_tool_function = getattr(toolkit, "call_tool_function")
|
||||
async_gen = await call_tool_function(tool_call=tool_call)
|
||||
last_chunk = None
|
||||
async for chunk in async_gen:
|
||||
last_chunk = chunk
|
||||
assert last_chunk is not None
|
||||
content = getattr(last_chunk, "content", None)
|
||||
assert isinstance(content, list) and content
|
||||
first = content[0]
|
||||
if isinstance(first, dict):
|
||||
text = first.get("text")
|
||||
else:
|
||||
text = getattr(first, "text", None)
|
||||
assert isinstance(text, str)
|
||||
payload = json.loads(text)
|
||||
assert isinstance(payload, dict)
|
||||
return payload
|
||||
|
||||
|
||||
class _SmokeRunner:
|
||||
async def run_json_stage(
|
||||
self,
|
||||
*,
|
||||
stage_config: RuntimeStageConfig,
|
||||
agent_name: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
toolkit: object | None,
|
||||
) -> dict[str, object]:
|
||||
del agent_name, system_prompt, user_prompt
|
||||
if stage_config.stage == "intent":
|
||||
return {
|
||||
"route": "TASK_EXECUTION",
|
||||
"intent_summary": "run calendar smoke flow",
|
||||
"direct_response": None,
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "smoke-task-1",
|
||||
"title": "calendar create-read-delete",
|
||||
"objective": "verify toolkit calendar write/read/delete calls",
|
||||
}
|
||||
],
|
||||
"complexity": "complex",
|
||||
}
|
||||
|
||||
if stage_config.stage == "execution":
|
||||
assert toolkit is not None
|
||||
created = await _invoke_tool(
|
||||
toolkit,
|
||||
tool_name="calendar.write",
|
||||
tool_input={
|
||||
"operation": "create",
|
||||
"title": "agentscope smoke event",
|
||||
"description": "agentscope runtime smoke",
|
||||
"start_at": datetime.now(timezone.utc).isoformat(),
|
||||
"timezone": "Asia/Shanghai",
|
||||
},
|
||||
)
|
||||
created_data = created.get("data")
|
||||
assert isinstance(created_data, dict)
|
||||
created_id = created_data.get("id")
|
||||
assert isinstance(created_id, str) and created_id
|
||||
|
||||
read_payload = await _invoke_tool(
|
||||
toolkit,
|
||||
tool_name="calendar.read",
|
||||
tool_input={"page": 1, "page_size": 10},
|
||||
)
|
||||
read_data = read_payload.get("data")
|
||||
assert isinstance(read_data, dict)
|
||||
items = read_data.get("items")
|
||||
assert isinstance(items, list)
|
||||
|
||||
deleted = await _invoke_tool(
|
||||
toolkit,
|
||||
tool_name="calendar.write",
|
||||
tool_input={"operation": "delete", "event_id": created_id},
|
||||
)
|
||||
deleted_data = deleted.get("data")
|
||||
assert isinstance(deleted_data, dict)
|
||||
assert deleted_data.get("ok") is True
|
||||
|
||||
return {
|
||||
"task_id": "smoke-task-1",
|
||||
"status": "SUCCESS",
|
||||
"execution_summary": "calendar create-read-delete succeeded",
|
||||
"execution_data": {
|
||||
"created_id": created_id,
|
||||
"read_item_count": len(items),
|
||||
},
|
||||
"user_feedback_needs": [],
|
||||
}
|
||||
|
||||
return {
|
||||
"assistant_text": "agentscope smoke completed",
|
||||
"response_metadata": {"source": "smoke-runner"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.live
|
||||
async def test_agentscope_runtime_calendar_smoke() -> None:
|
||||
if os.getenv("AGENTSCOPE_RUNTIME_SMOKE") != "1":
|
||||
pytest.skip("set AGENTSCOPE_RUNTIME_SMOKE=1 to run live smoke test")
|
||||
|
||||
user_id_raw = os.getenv("AGENTSCOPE_SMOKE_USER_ID", "").strip()
|
||||
user_token = os.getenv("AGENTSCOPE_SMOKE_USER_TOKEN", "").strip()
|
||||
if not user_id_raw or not user_token:
|
||||
pytest.fail(
|
||||
"AGENTSCOPE_RUNTIME_SMOKE=1 requires AGENTSCOPE_SMOKE_USER_ID and AGENTSCOPE_SMOKE_USER_TOKEN"
|
||||
)
|
||||
|
||||
owner_id = UUID(user_id_raw)
|
||||
|
||||
async def _fake_config_loader(_session: object) -> dict[str, RuntimeStageConfig]:
|
||||
return _runtime_stage_config()
|
||||
|
||||
orchestrator = AgentScopeRuntimeOrchestrator(
|
||||
runner=_SmokeRunner(),
|
||||
config_loader=_fake_config_loader,
|
||||
)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await orchestrator.run(
|
||||
session=session,
|
||||
owner_id=owner_id,
|
||||
user_token=user_token,
|
||||
user_context=_build_user_context(owner_id),
|
||||
user_input="run smoke",
|
||||
)
|
||||
|
||||
assert result.intent.route == "TASK_EXECUTION"
|
||||
assert result.execution is not None
|
||||
assert result.execution.overall_status == "SUCCESS"
|
||||
assert result.report.assistant_text == "agentscope smoke completed"
|
||||
@@ -10,8 +10,6 @@ from fastapi.testclient import TestClient
|
||||
from app import app
|
||||
from v1.inbox_messages.dependencies import get_inbox_message_service
|
||||
from v1.inbox_messages.schemas import (
|
||||
InboxMessageAcceptRequest,
|
||||
InboxMessageListRequest,
|
||||
InboxMessageResponse,
|
||||
InboxMessageStatus,
|
||||
InboxMessageType,
|
||||
@@ -23,37 +21,22 @@ class FakeInboxMessageService:
|
||||
def __init__(
|
||||
self,
|
||||
messages: list[InboxMessageResponse],
|
||||
accepted: InboxMessageResponse,
|
||||
dismissed: InboxMessageResponse,
|
||||
read_message: InboxMessageResponse,
|
||||
) -> None:
|
||||
self._messages = messages
|
||||
self._accepted = accepted
|
||||
self._dismissed = dismissed
|
||||
self._read_message = read_message
|
||||
|
||||
async def list_messages(
|
||||
self, request: InboxMessageListRequest
|
||||
self, is_read: bool | None = None
|
||||
) -> list[InboxMessageResponse]:
|
||||
if request.status is None:
|
||||
if is_read is None:
|
||||
return self._messages
|
||||
return [
|
||||
message for message in self._messages if message.status == request.status
|
||||
]
|
||||
return [message for message in self._messages if message.is_read is is_read]
|
||||
|
||||
async def accept_invitation(
|
||||
self,
|
||||
message_id: UUID,
|
||||
request: InboxMessageAcceptRequest,
|
||||
) -> InboxMessageResponse:
|
||||
if message_id != self._accepted.id:
|
||||
async def mark_as_read(self, message_id: UUID) -> InboxMessageResponse:
|
||||
if message_id != self._read_message.id:
|
||||
raise HTTPException(status_code=404, detail="Inbox message not found")
|
||||
if not request.permission_view:
|
||||
raise HTTPException(status_code=400, detail="permission_view is required")
|
||||
return self._accepted
|
||||
|
||||
async def dismiss_invitation(self, message_id: UUID) -> InboxMessageResponse:
|
||||
if message_id != self._dismissed.id:
|
||||
raise HTTPException(status_code=404, detail="Inbox message not found")
|
||||
return self._dismissed
|
||||
return self._read_message
|
||||
|
||||
|
||||
def _override_inbox_message_service(
|
||||
@@ -84,11 +67,11 @@ def _build_message(
|
||||
|
||||
def test_list_inbox_messages_returns_200() -> None:
|
||||
pending_message = _build_message(uuid4(), InboxMessageStatus.PENDING)
|
||||
accepted_message = _build_message(uuid4(), InboxMessageStatus.ACCEPTED)
|
||||
read_message = _build_message(uuid4(), InboxMessageStatus.ACCEPTED)
|
||||
read_message = read_message.model_copy(update={"is_read": True})
|
||||
service = FakeInboxMessageService(
|
||||
messages=[pending_message, accepted_message],
|
||||
accepted=accepted_message,
|
||||
dismissed=_build_message(uuid4(), InboxMessageStatus.DISMISSED),
|
||||
messages=[pending_message, read_message],
|
||||
read_message=read_message,
|
||||
)
|
||||
app.dependency_overrides[get_inbox_message_service] = (
|
||||
_override_inbox_message_service(service)
|
||||
@@ -96,21 +79,21 @@ def test_list_inbox_messages_returns_200() -> None:
|
||||
|
||||
client = TestClient(app)
|
||||
try:
|
||||
response = client.get("/api/v1/inbox/messages", params={"status": "pending"})
|
||||
response = client.get("/api/v1/inbox/messages", params={"is_read": "false"})
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["status"] == "pending"
|
||||
assert body[0]["is_read"] is False
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_accept_inbox_message_returns_200() -> None:
|
||||
accepted_message = _build_message(uuid4(), InboxMessageStatus.ACCEPTED)
|
||||
def test_mark_as_read_returns_200() -> None:
|
||||
read_message = _build_message(uuid4(), InboxMessageStatus.PENDING)
|
||||
read_message = read_message.model_copy(update={"is_read": True})
|
||||
service = FakeInboxMessageService(
|
||||
messages=[accepted_message],
|
||||
accepted=accepted_message,
|
||||
dismissed=_build_message(uuid4(), InboxMessageStatus.DISMISSED),
|
||||
messages=[read_message],
|
||||
read_message=read_message,
|
||||
)
|
||||
app.dependency_overrides[get_inbox_message_service] = (
|
||||
_override_inbox_message_service(service)
|
||||
@@ -118,39 +101,10 @@ def test_accept_inbox_message_returns_200() -> None:
|
||||
|
||||
client = TestClient(app)
|
||||
try:
|
||||
response = client.post(
|
||||
f"/api/v1/inbox/messages/{accepted_message.id}/accept",
|
||||
json={
|
||||
"permission_view": True,
|
||||
"permission_edit": True,
|
||||
"permission_invite": False,
|
||||
},
|
||||
)
|
||||
response = client.patch(f"/api/v1/inbox/messages/{read_message.id}/read")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["id"] == str(accepted_message.id)
|
||||
assert body["status"] == "accepted"
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_dismiss_inbox_message_returns_200() -> None:
|
||||
dismissed_message = _build_message(uuid4(), InboxMessageStatus.DISMISSED)
|
||||
service = FakeInboxMessageService(
|
||||
messages=[dismissed_message],
|
||||
accepted=_build_message(uuid4(), InboxMessageStatus.ACCEPTED),
|
||||
dismissed=dismissed_message,
|
||||
)
|
||||
app.dependency_overrides[get_inbox_message_service] = (
|
||||
_override_inbox_message_service(service)
|
||||
)
|
||||
|
||||
client = TestClient(app)
|
||||
try:
|
||||
response = client.post(f"/api/v1/inbox/messages/{dismissed_message.id}/dismiss")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["id"] == str(dismissed_message.id)
|
||||
assert body["status"] == "dismissed"
|
||||
assert body["id"] == str(read_message.id)
|
||||
assert body["is_read"] is True
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
Reference in New Issue
Block a user