feat: 增强日历功能并集成 AgentScope 代理服务

This commit is contained in:
qzl
2026-03-11 15:28:29 +08:00
parent e55e445906
commit e20e7d2a02
85 changed files with 5175 additions and 885 deletions
@@ -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,
)